From b3421c3e4091d56c23189765ead74346c4e4aa1f Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Mon, 20 Jul 2026 17:29:45 -0400 Subject: [PATCH] v0.0.57 - Migrated to unified nostr_ prefixed verb names; removed legacy verb aliases (sign_data, ssh_sign, verify_signature, kem_encapsulate, kem_decapsulate, otp_encrypt, otp_decrypt); split get_public_key into algorithm-based get_public_key and role-based nostr_get_public_key; OTP now selected via algorithm:otp instead of curve:otp; consolidated API docs from api.md into README.md --- Makefile | 16 +- README.md | 843 ++++++++++-------------- api.md | 387 +---------- client/README.md | 55 +- client/demo_c99.c | 20 +- client/demo_javascript.js | 26 +- client/demo_python.py | 30 +- examples/feather_sign_event.py | 4 +- examples/feather_webusb_demo.html | 10 +- examples/get_pubkey_tcp.c | 2 +- examples/get_public_key_client.c | 2 +- examples/kb2040_hidden_signer_client.py | 2 +- examples/otp_nostr_30078.py | 6 +- examples/pq_kem_example.c | 13 +- examples/pq_sign_example.c | 8 +- examples/sign_event_client.c | 2 +- examples/ssh_sign_example.c | 8 +- plans/legacy_verb_aliases.md | 4 +- src/dispatcher.c | 756 +++------------------ src/enforcement.c | 174 ++--- src/key_store.c | 20 +- src/main.c | 19 +- src/mnemonic.c | 15 +- src/otp_pad.c | 2 +- src/otp_pad.h | 4 +- src/policy.c | 15 +- src/pq_crypto.c | 15 +- src/role_table.c | 15 +- src/secure_mem.c | 15 +- src/selector.c | 15 +- src/server.c | 21 +- src/socket_name.c | 15 +- tests/test.sh | 22 +- tests/test_algorithm_api.c | 62 +- tests/test_auth_envelope.c | 8 +- tests/test_dispatcher.c | 45 +- tests/test_ed25519_x25519.c | 115 ++-- tests/test_enforcement.c | 51 +- tests/test_integration.c | 35 +- tests/test_mine_event.c | 72 +- tests/test_ml_dsa_65.c | 82 +-- tests/test_ml_kem_768.c | 77 +-- tests/test_mnemonic.c | 15 +- tests/test_policy.c | 71 +- tests/test_pq_crypto.c | 13 +- tests/test_pubkey_format.c | 49 +- tests/test_qrexec_auth.c | 8 +- tests/test_role_table.c | 15 +- tests/test_selector.c | 15 +- tests/test_slh_dsa_128s.c | 78 ++- tests/test_socket_name.c | 15 +- 51 files changed, 1105 insertions(+), 2282 deletions(-) diff --git a/Makefile b/Makefile index 83577c0..10ebc4c 100644 --- a/Makefile +++ b/Makefile @@ -182,7 +182,7 @@ examples: $(EXAMPLE_GET_PUBLIC_KEY_TARGET) $(EXAMPLE_SIGN_EVENT_TARGET) $(EXAMPL $(TEST_MNEMONIC_TARGET): $(TEST_DIR)/test_mnemonic.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c @mkdir -p $(BUILD_DIR) - $(CC) $(CFLAGS) $(TEST_DIR)/test_mnemonic.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_MNEMONIC_TARGET) $(LDFLAGS) + $(CC) $(CFLAGS) $(TEST_DIR)/test_mnemonic.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_MNEMONIC_TARGET) $(LDFLAGS) $(TEST_MNEMONIC_INPUT_TARGET): $(TEST_DIR)/test_mnemonic_input.c @mkdir -p $(BUILD_DIR) @@ -202,7 +202,7 @@ $(TEST_ENFORCEMENT_TARGET): $(TEST_DIR)/test_enforcement.c $(SRC_DIR)/enforcemen $(TEST_DISPATCHER_TARGET): $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/key_store.c $(SRC_DIR)/miner.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c @mkdir -p $(BUILD_DIR) - $(CC) $(CFLAGS) $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/key_store.c $(SRC_DIR)/miner.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_DISPATCHER_TARGET) $(LDFLAGS) + $(CC) $(CFLAGS) $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/key_store.c $(SRC_DIR)/miner.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_DISPATCHER_TARGET) $(LDFLAGS) $(TEST_POLICY_TARGET): $(TEST_DIR)/test_policy.c $(SRC_DIR)/policy.c @mkdir -p $(BUILD_DIR) @@ -226,7 +226,7 @@ $(TEST_QREXEC_AUTH_TARGET): $(TEST_DIR)/test_qrexec_auth.c $(SRC_DIR)/auth_envel $(TEST_MINE_EVENT_TARGET): $(TEST_DIR)/test_mine_event.c $(SRC_DIR)/miner.c $(SRC_DIR)/key_store.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/dispatcher.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c @mkdir -p $(BUILD_DIR) - $(CC) $(CFLAGS) $(TEST_DIR)/test_mine_event.c $(SRC_DIR)/miner.c $(SRC_DIR)/key_store.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/dispatcher.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_MINE_EVENT_TARGET) $(LDFLAGS) + $(CC) $(CFLAGS) $(TEST_DIR)/test_mine_event.c $(SRC_DIR)/miner.c $(SRC_DIR)/key_store.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/dispatcher.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_MINE_EVENT_TARGET) $(LDFLAGS) $(TEST_PQ_CRYPTO_TARGET): $(TEST_DIR)/test_pq_crypto.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/role_table.c @mkdir -p $(BUILD_DIR) @@ -234,23 +234,23 @@ $(TEST_PQ_CRYPTO_TARGET): $(TEST_DIR)/test_pq_crypto.c $(SRC_DIR)/pq_crypto.c $( $(TEST_ED25519_X25519_TARGET): $(TEST_DIR)/test_ed25519_x25519.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c @mkdir -p $(BUILD_DIR) - $(CC) $(CFLAGS) $(TEST_DIR)/test_ed25519_x25519.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_ED25519_X25519_TARGET) $(LDFLAGS) + $(CC) $(CFLAGS) $(TEST_DIR)/test_ed25519_x25519.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_ED25519_X25519_TARGET) $(LDFLAGS) $(TEST_ML_DSA_65_TARGET): $(TEST_DIR)/test_ml_dsa_65.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c @mkdir -p $(BUILD_DIR) - $(CC) $(CFLAGS) $(TEST_DIR)/test_ml_dsa_65.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_ML_DSA_65_TARGET) $(LDFLAGS) + $(CC) $(CFLAGS) $(TEST_DIR)/test_ml_dsa_65.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_ML_DSA_65_TARGET) $(LDFLAGS) $(TEST_SLH_DSA_128S_TARGET): $(TEST_DIR)/test_slh_dsa_128s.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c @mkdir -p $(BUILD_DIR) - $(CC) $(CFLAGS) $(TEST_DIR)/test_slh_dsa_128s.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_SLH_DSA_128S_TARGET) $(LDFLAGS) + $(CC) $(CFLAGS) $(TEST_DIR)/test_slh_dsa_128s.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_SLH_DSA_128S_TARGET) $(LDFLAGS) $(TEST_ML_KEM_768_TARGET): $(TEST_DIR)/test_ml_kem_768.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c @mkdir -p $(BUILD_DIR) - $(CC) $(CFLAGS) $(TEST_DIR)/test_ml_kem_768.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_ML_KEM_768_TARGET) $(LDFLAGS) + $(CC) $(CFLAGS) $(TEST_DIR)/test_ml_kem_768.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_ML_KEM_768_TARGET) $(LDFLAGS) $(TEST_PUBKEY_FORMAT_TARGET): $(TEST_DIR)/test_pubkey_format.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c @mkdir -p $(BUILD_DIR) - $(CC) $(CFLAGS) $(TEST_DIR)/test_pubkey_format.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_PUBKEY_FORMAT_TARGET) $(LDFLAGS) + $(CC) $(CFLAGS) $(TEST_DIR)/test_pubkey_format.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_PUBKEY_FORMAT_TARGET) $(LDFLAGS) $(TEST_ALGORITHM_API_TARGET): $(TEST_DIR)/test_algorithm_api.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/policy.c @mkdir -p $(BUILD_DIR) diff --git a/README.md b/README.md index dc25480..dd0693d 100644 --- a/README.md +++ b/README.md @@ -29,37 +29,15 @@ You run it when you need signing. You stop it when you are done. Closing the ter ### 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. +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. +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. ### 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. +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 @@ -83,16 +61,12 @@ When started, `n_signer` immediately enters terminal input mode: - On `E`: prompt for mnemonic with terminal echo disabled, then validate. - On `G`: generate a fresh 12-word BIP-39 mnemonic from `getrandom(2)`, display it numbered with a "WRITE THIS DOWN — IT WILL NOT BE SHOWN AGAIN" warning, then continue. There is no confirmation step. 2. Build in-memory role/selector state from the mnemonic. -3. **Interactive transport selection** (if no `--listen` flag given and stdin is a TTY): choose one or more of: - - Local Unix socket - - Qubes qrexec bridge - - FIPS/TCP listener (framed JSON) - - HTTP listener (curl-friendly) +3. **Interactive transport selection** (if no `--listen` flag given and stdin is a TTY): choose one or more of: Local Unix socket, Qubes qrexec bridge, FIPS/TCP listener (framed JSON), HTTP listener (curl-friendly). 4. **Index whitelist** (optional): restrict which `nostr_index` values this session can access. 5. **OTP pad selection** (optional): auto-scans attached USB drives for OTP pads and offers to bind one. See [`plans/otp_nostr_integration.md`](plans/otp_nostr_integration.md). 6. Pick the abstract socket name (random BIP-39 pair, or `--socket-name` / `--name` / `-n` override). 7. Initialize transport endpoints and bind the socket. -8. Switch to running status display, with the signer name and socket address shown in the Connections section and status line (see [§3.2](#32-running-phase-status-display--signer)). +8. Switch to running status display. No startup files are read or written. The mnemonic — typed or generated — lives only in `mlock`'d memory and is zeroized on shutdown or crash. @@ -105,7 +79,7 @@ These modes avoid putting mnemonic material in argv/environment and are designed ### 3.2 Running phase (status display + signer) -After unlock, the terminal becomes a live status and control console rendered by [`render_status()`](src/main.c). The top frame shows the program name and version; below it are the Roles and Activity sections, followed by a single status line. Connection instructions are not shown by default — press `d` to display them on demand (see below). Example layout: +After unlock, the terminal becomes a live status and control console rendered by [`render_status()`](src/main.c). The top frame shows the program name and version; below it are the Roles and Activity sections, followed by a single status line. Connection instructions are not shown by default — press `d` to display them on demand. Example layout: ```text n_signer v0.0.53 > Main Menu @@ -117,10 +91,10 @@ nostr_idx_1 nostr secp256k1 m/44'/1237'/1'/0/0 backup bitcoin secp256k1 m/84'/0'/0'/0/5 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 +16:03:11 allow caller=uid:1000 method=nostr_get_public_key role=main +16:02:44 prompt caller=uid:1000 method=nostr_sign_event role=ops +16:02:46 allow caller=uid:1000 method=nostr_sign_event role=ops +15:59:10 deny caller=uid:1001 method=nostr_sign_event error=unauthorized session=unlocked (12 words) signer=nsigner_hairy_dog derived=3 auto-approve=OFF @@ -133,53 +107,12 @@ q/x quit The **Derivation path** column shows the full BIP-44 path for each role's key. For `nostr_index` roles this is `m/44'/1237'/'/0/0` (NIP-06); for `role_path` roles it's the explicit path. -The signer's name (`nsigner_hairy_dog` in this example) appears in the **status line** at the bottom (`signer=nsigner_hairy_dog`). See [§7.1](#71-linux-desktop-abstract-namespace-unix-socket) for how the name is generated. +The signer's name (`nsigner_hairy_dog` in this example) appears in the **status line** at the bottom (`signer=nsigner_hairy_dog`). See [§4.1](#41-linux-desktop-abstract-namespace-unix-socket) for how the name is generated. ### Connection instructions (press `d`) Pressing `d` clears the screen and shows each active transport as a titled block with the connection string and an example client command. Press any key to return to the status display. -```text -n_signer v0.0.53 > Connection Instructions - - -Unix socket: - - @nsigner_hairy_dog - - Example: - nsigner --socket-name nsigner_hairy_dog client '' - - Qrexec (bridge-source-trusted): - qrexec-client-vm qubes.NsignerRpc - - -FIPS: - - http://npub10vt4scusw6lq27qw83nfwp5sqer492h0tnwa8ugqjvp6l4xuz2qsdycrwd.fips:11111 - - Example: - curl -X POST http://npub10vt4scusw6lq27qw83nfwp5sqer492h0tnwa8ugqjvp6l4xuz2qsdycrwd.fips:11111/ - -H 'Content-Type: application/json' - -d '' - - -HTTP: - - http://127.0.0.1:11112 - - Example: - curl -X POST http://127.0.0.1:11112/ - -H 'Content-Type: application/json' - -d '' - -OTP pad: 333e9902db839d9d... (offset 288 / 1048576 bytes) - -session=unlocked (12 words) signer=nsigner_hairy_dog derived=3 auto-approve=OFF - -Press any key to return -``` - Hotkeys (active while the status display is shown): - `a` — toggle auto-approve (prompt) for this session @@ -192,12 +125,10 @@ Hotkeys (active while the status display is shown): 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 +method: nostr_sign_event selector: role=ops purpose/curve: nostr/secp256k1 @@ -212,378 +143,349 @@ No response is emitted to caller until the local user decides. - `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 +## 4. API -`n_signer` derives many role-scoped keys from one mnemonic root. Requests select a role using explicit selectors, then enforcement checks operation compatibility. +`n_signer` exposes a JSON-RPC 2.0-style request/response protocol. Every request is a single JSON object; every response is a single JSON object. This section is the complete, authoritative description of the API. -### 4.1 Nostr shorthand: nostr_index +For the migration plan from the legacy verb names, see [`plans/legacy_verb_aliases.md`](plans/legacy_verb_aliases.md). -Nostr shorthand keeps the explicit index selector: +### 4.1 Request format -`m/44'/1237'/'/0/0` +```json +{ "id": "", "method": "", "params": [ , , ..., { } ] } +``` -Use `nostr_index` only for Nostr-indexed roles. +- `id` — caller-supplied string echoed verbatim in the response. Used to match requests to responses. +- `method` — the verb name (see [§4.2](#42-verbs)). +- `params` — a JSON array. Positional arguments come first; the **last array element** is conventionally an options object. The options object is optional for most verbs. -### 4.2 Full derivation path: role_path +### 4.2 Response format -For non-Nostr or advanced layouts, caller may use full `role_path` selector. +Success: +```json +{ "id": "", "result": } +``` -Security rule: `role_path` must match a pre-registered role entry. Unregistered ad-hoc derivation requests are rejected. +`result` is a JSON string. For structured verbs the string is itself a serialized JSON object — clients should `JSON.parse` it. -### 4.3 What memorizing your seed phrase gets you +Error: +```json +{ "id": "", "error": { "code": , "message": "" } } +``` -A single memorized mnemonic can deterministically recover multiple key domains through role definitions, not just one identity. +Error codes: -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. +| Code | Message | Meaning | +|-------|-------------------------------|--------------------------------------------------------------------| +| -32700| `parse_error` | Request was not valid JSON. | +| -32600| `invalid_request` | Missing `id`, `method`, or `params`, or `params` is not an array. | +| -32601| `method_not_found` | Unknown verb, or verb not valid for the selected algorithm. | +| -32602| `invalid_params` | Malformed arguments (bad hex, wrong length, missing field, etc.). | +| 1001 | `ambiguous_role_selector` | More than one role selector was supplied. | +| 1002 | `unknown_role` | No role matched the selector. | +| 1003 | `no_default_role` | No selector given and no `main` role exists. | +| 1004 | `purpose_mismatch` | Role's purpose is not valid for this verb. | +| 1005 | `curve_mismatch` | Role's curve is not valid for this verb. | +| 1006 | `mnemonic_not_loaded` | No mnemonic is loaded in the signer. | +| 1007 | `no_termination_condition` | `nostr_mine_event` called without `difficulty` or `timeout_sec`. | +| 1008 | `mining_failed` | Internal error during proof-of-work mining. | +| 1009 | `not_yet_implemented` | Verb+algorithm combination is reserved but not yet implemented. | +| 1010 | `algorithm_not_supported_for_verb` | The `algorithm` value is not valid for this verb. | -## 4b. Crypto palette +### 4.3 Verbs -`n_signer` supports six cryptographic algorithms, all derived deterministically from the same BIP-39 mnemonic. Each algorithm is bound to a specific `(purpose, curve)` pair and a distinct BIP-44 derivation path. +All verbs take their arguments as positional `params` and their options in a trailing options object. Most verbs select a key via the `algorithm` + `index` options (see [§4.4](#44-algorithms)). The `nostr_*` verbs select a secp256k1 NIP-06 key via `nostr_index` and implement Nostr-protocol-specific serialization on top of the raw crypto. -### 4b.1 Algorithms +| Verb | Algorithms | Positional params | Options | +|-------------------------|-----------------------------------------------|----------------------------------|----------------------------------| +| `get_public_key` | all key-deriving algorithms | — | `algorithm`, `index` | +| `sign` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | `` | `algorithm`, `index`, `scheme`* | +| `verify` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | ``, `` | `algorithm`, `index`, `scheme`* | +| `encapsulate` | ml-kem-768 | `` | `algorithm` | +| `decapsulate` | ml-kem-768 | `` | `algorithm`, `index` | +| `derive_shared_secret` | x25519 | `` | `algorithm`, `index` | +| `encrypt` | otp | `` | `algorithm`, `encoding` | +| `decrypt` | otp | `` | `algorithm`, `encoding` | +| `nostr_get_public_key` | secp256k1 (NIP-06) | — | `nostr_index`, `format` | +| `nostr_sign_event` | secp256k1 (NIP-06) | `` | `nostr_index` | +| `nostr_mine_event` | secp256k1 (NIP-06) | `` | `nostr_index`, `difficulty`, `timeout_sec`, `threads` | +| `nostr_nip04_encrypt` | secp256k1 (NIP-06) | ``, `` | `nostr_index` | +| `nostr_nip04_decrypt` | secp256k1 (NIP-06) | `<peer_pubkey_hex>`, `<ciphertext>` | `nostr_index` | +| `nostr_nip44_encrypt` | secp256k1 (NIP-06) | `<peer_pubkey_hex>`, `<plaintext>` | `nostr_index` | +| `nostr_nip44_decrypt` | secp256k1 (NIP-06) | `<peer_pubkey_hex>`, `<ciphertext>` | `nostr_index` | -| Algorithm | Curve label | Purpose | FIPS standard | Key sizes (priv / pub) | Derivation path | -|---|---|---|---|---|---| -| secp256k1 | `secp256k1` | `nostr` | — | 32 / 32 bytes | `m/44'/1237'/<n>'/0/0` (NIP-06) | -| ed25519 | `ed25519` | `ssh` | — | 32 / 32 bytes | `m/44'/102001'/<n>'/0'/0'` (SLIP-0010) | -| x25519 | `x25519` | `age` | — | 32 / 32 bytes | `m/44'/102002'/<n>'/0'/0'` (SLIP-0010) | -| ML-DSA-65 | `ml-dsa-65` | `pq-sig` | FIPS 204 | 4032 / 1952 bytes | `m/44'/102003'/<n>'/0'/0'` → seed → DRBG → PQClean keygen | -| SLH-DSA-128s | `slh-dsa-128s` | `pq-sig` | FIPS 205 | 64 / 32 bytes | `m/44'/102004'/<n>'/0'/0'` → seed → DRBG → PQClean keygen | -| ML-KEM-768 | `ml-kem-768` | `pq-kem` | FIPS 203 | 2400 / 1184 bytes | `m/44'/102005'/<n>'/0'/0'` → seed → DRBG → PQClean keygen | +\* `scheme` is secp256k1-only: `"schnorr"` (default, BIP-340) or `"ecdsa"`. -All six algorithms are always compiled in on every target (host x86_64 static binary and ESP32 firmware). The PQ implementations are vendored from [PQClean](https://github.com/PQClean/PQClean) (public domain / CC0). +#### Enforcement matrix -### 4b.2 Key derivation +| Verb | Valid algorithms | +|----------------------------|-----------------------------------------------| +| `sign` / `verify` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | +| `encapsulate` / `decapsulate` | ml-kem-768 | +| `derive_shared_secret` | x25519 | +| `encrypt` / `decrypt` | otp | +| `get_public_key` | all key-deriving algorithms | +| `nostr_*` | secp256k1 (Nostr protocol) | -All keys derive deterministically from the BIP-39 mnemonic: +Any unlisted `(verb, algorithm)` pair is rejected with `algorithm_not_supported_for_verb` (1010). + +### 4.4 Algorithms + +All keys derive deterministically from the loaded BIP-39 mnemonic. The caller selects an algorithm by name and a derivation `index` (an integer `<n>` substituted into the algorithm's derivation path). OTP is the exception — it does not derive a key, it consumes a bound one-time pad (see [§4.4.3](#443-otp)). + +#### 4.4.1 Algorithm table + +| Algorithm | Key type | FIPS standard | Derivation path | Key sizes (priv / pub, bytes) | +|-----------------|-----------------|---------------|---------------------------------------|-------------------------------| +| `secp256k1` | Signature | — | `m/44'/1237'/<n>'/0/0` (NIP-06) | 32 / 32 | +| `ed25519` | Signature | — | `m/44'/102001'/<n>'/0/0'` (SLIP-0010) | 32 / 32 | +| `x25519` | Key agreement | — | `m/44'/102002'/<n>'/0/0'` (SLIP-0010) | 32 / 32 | +| `ml-dsa-65` | PQ signature | FIPS 204 | `m/44'/102003'/<n>'/0/0'` → DRBG | 4032 / 1952 | +| `slh-dsa-128s` | PQ signature | FIPS 205 | `m/44'/102004'/<n>'/0/0'` → DRBG | 64 / 32 | +| `ml-kem-768` | PQ KEM | FIPS 203 | `m/44'/102005'/<n>'/0/0'` → DRBG | 2400 / 1184 | +| `otp` | One-time pad | — | (no key — bound USB pad) | n/a | + +#### 4.4.2 Key derivation - **secp256k1** uses standard BIP-32/NIP-06 derivation. The 32-byte path output is the private key scalar. - **ed25519 / x25519** use SLIP-0010 HMAC-SHA512 derivation (all-hardened paths, as required by SLIP-0010 for ed25519). The 32-byte output is the private key. -- **PQ algorithms** (ML-DSA-65, SLH-DSA-128s, ML-KEM-768) use a two-stage approach: the mnemonic-derived 32-byte seed feeds a SHAKE-256 DRBG (NIST SP 800-90A style), which replaces PQClean's `randombytes()` callback during keygen. This produces deterministic PQ key pairs from the mnemonic — same mnemonic, same role, same key pair every time. See [`documents/SECURITY.md`](documents/SECURITY.md) §17 for the security argument. +- **PQ algorithms** (ML-DSA-65, SLH-DSA-128s, ML-KEM-768) use a two-stage approach: the mnemonic-derived 32-byte seed feeds a SHAKE-256 DRBG (NIST SP 800-90A style), which replaces PQClean's `randombytes()` callback during keygen. Same mnemonic, same index, same key pair every time. +- **otp** does not derive a key. A pad is bound at signer startup (`--otp-pad-dir` + `--otp-pad`); the pad offset advances monotonically across requests. -The new algorithms use BIP-44 coin types `102001`–`102005` (unregistered in SLIP-44, chosen to avoid collisions with real cryptocurrencies). All non-secp256k1 paths are fully hardened per SLIP-0010. +The PQ implementations are vendored from [PQClean](https://github.com/PQClean/PQClean) (public domain / CC0). All six algorithms are always compiled in on every target. The three post-quantum algorithms address the **harvest-now-decrypt-later** threat: an adversary recording encrypted traffic today to decrypt it once a quantum computer becomes available. -### 4b.3 Post-quantum context +#### 4.4.3 OTP -The three post-quantum algorithms address the **harvest-now-decrypt-later** threat: an adversary recording encrypted traffic today to decrypt it once a quantum computer becomes available. ML-KEM-768 protects key agreement against this threat. ML-DSA-65 and SLH-DSA-128s protect signatures against future quantum forgery. +The `otp` algorithm is a stream-style one-time pad, not a key-derivation scheme. It is selected like any other algorithm via `{"algorithm":"otp"}` and works with the `encrypt` / `decrypt` verbs. One pad per session; the pad offset advances monotonically across requests and is reported in every response. -All three are FIPS-standardized (FIPS 203, 204, 205) and are provided as **additional options**, not replacements for secp256k1. Nostr continues to use secp256k1 exclusively. PQ algorithms are opt-in per role. +### 4.5 Examples -Note: OpenSSH does not yet support PQ signing keys. The `pq-sig` purpose is forward-looking — the primitives are ready for when the ecosystem adopts them. - -### 4b.4 Structured `get_public_key` response - -For secp256k1, `get_public_key` returns the plain 64-hex-char public key string (backward compatible). An optional `{"format": "structured"}` in the options object requests a structured JSON response. - -For all other algorithms (ed25519, x25519, ML-DSA-65, SLH-DSA-128s, ML-KEM-768), `get_public_key` always returns a structured JSON object (serialized as a string): +#### `get_public_key` ```json -{"algorithm": "ml-dsa-65", "public_key": "<hex>", "key_id": "<16 hex chars>"} +{ "id": "1", "method": "get_public_key", "params": [ { "algorithm": "ml-dsa-65", "index": 0 } ] } ``` -The `key_id` is a short display identifier (first 16 hex chars of the public key). PQ public keys are large (ML-DSA-65: 3904 hex chars; ML-KEM-768: 2368 hex chars). - -## 4c. Algorithm-based API - -In addition to the role-based API, n_signer supports an **algorithm-based API** where the caller specifies the algorithm and derivation index directly, without needing to know role names. This is the preferred API for new clients. - -### 4c.1 Verbs - -| Verb | Description | Algorithm parameter | Key parameter | -|---|---|---|---| -| `sign` | Sign arbitrary bytes | `algorithm` | `index` | -| `verify` | Verify a signature | `algorithm` | `index` | -| `encapsulate` | KEM encapsulation | `algorithm` | `public_key` (peer's) | -| `decapsulate` | KEM decapsulation | `algorithm` | `index` | -| `derive_shared_secret` | ECDH key agreement (x25519) | `algorithm` | `index` + `peer_public_key` | -| `get_public_key` | Get public key (algorithm-based) | `algorithm` | `index` | - -### 4c.2 Algorithm names - -| String | Algorithm | Key type | -|---|---|---| -| `secp256k1` | secp256k1 (Schnorr/ECDSA) | Signature | -| `ed25519` | ed25519 | Signature | -| `ml-dsa-65` | ML-DSA-65 (FIPS 204) | Signature | -| `slh-dsa-128s` | SLH-DSA-128s (FIPS 205) | Signature | -| `x25519` | X25519 (ECDH) | Key agreement | -| `ml-kem-768` | ML-KEM-768 (FIPS 203) | KEM | - -### 4c.3 Request examples - -**Sign with ed25519:** +Response: ```json -{"id":"1","method":"sign","params":["68656c6c6f",{"algorithm":"ed25519","index":0}]} +{ "id": "1", "result": "{\"algorithm\":\"ml-dsa-65\",\"public_key\":\"<hex>\",\"key_id\":\"<16 hex>\"}" } ``` -Response: `{"id":"1","result":{"signature":"<hex>","algorithm":"ed25519","key_id":"<16hex>"}}` -**Sign with secp256k1 (ECDSA scheme):** +`key_id` is the first 16 hex characters of the public key — a short display identifier. + +#### `sign` + ```json -{"id":"2","method":"sign","params":["68656c6c6f",{"algorithm":"secp256k1","index":0,"scheme":"ecdsa"}]} +{ "id": "2", "method": "sign", "params": [ "68656c6c6f", { "algorithm": "ed25519", "index": 0 } ] } ``` -The `scheme` parameter is optional for secp256k1: `"schnorr"` (default, BIP-340) or `"ecdsa"`. -**Get public key (algorithm-based):** +Response: ```json -{"id":"3","method":"get_public_key","params":[{"algorithm":"ml-dsa-65","index":0}]} +{ "id": "2", "result": "{\"signature\":\"<hex>\",\"algorithm\":\"ed25519\",\"key_id\":\"<16 hex>\"}" } ``` -Response: `{"id":"3","result":{"algorithm":"ml-dsa-65","public_key":"<hex>","key_id":"<16hex>"}}` -**KEM encapsulate:** +The first positional argument is the message as hex. For `secp256k1` the `scheme` option selects `"schnorr"` (default, BIP-340) or `"ecdsa"`: + ```json -{"id":"4","method":"encapsulate","params":["<peer_pubkey_hex>",{"algorithm":"ml-kem-768"}]} +{ "id": "3", "method": "sign", "params": [ "68656c6c6f", { "algorithm": "secp256k1", "index": 0, "scheme": "ecdsa" } ] } ``` -**KEM decapsulate:** +#### `verify` + ```json -{"id":"5","method":"decapsulate","params":["<ciphertext_hex>",{"algorithm":"ml-kem-768","index":0}]} +{ "id": "4", "method": "verify", "params": [ "<message_hex>", "<signature_hex>", { "algorithm": "ed25519", "index": 0 } ] } ``` -**ECDH shared secret (x25519):** +Response: ```json -{"id":"6","method":"derive_shared_secret","params":["<peer_pubkey_hex>",{"algorithm":"x25519","index":0}]} +{ "id": "4", "result": "{\"valid\":true,\"algorithm\":\"ed25519\"}" } ``` -### 4c.4 Verb aliases +The signer derives its own public key from `(algorithm, index)` and verifies against it. To verify an arbitrary third-party key, use a client-side library. -These verb aliases are also available. When used with the `algorithm` parameter, they map to the algorithm-based verbs: - -| Alias | Canonical verb | Default algorithm | -|---|---|---| -| `sign_data` | `sign` | (from `algorithm` parameter) | -| `ssh_sign` | `sign` | `ed25519` | -| `verify_signature` | `verify` | (from `algorithm` parameter) | -| `kem_encapsulate` | `encapsulate` | `ml-kem-768` | -| `kem_decapsulate` | `decapsulate` | `ml-kem-768` | - -Without the `algorithm` parameter, these aliases fall through to the role-based path (backward compatible). - -### 4c.5 Algorithm-based preapprove +#### `encapsulate` (ML-KEM-768) +```json +{ "id": "5", "method": "encapsulate", "params": [ "<peer_pubkey_hex>", { "algorithm": "ml-kem-768" } ] } ``` + +Response: +```json +{ "id": "5", "result": "{\"ciphertext\":\"<hex>\",\"shared_secret\":\"<hex>\",\"algorithm\":\"ml-kem-768\"}" } +``` + +`peer_pubkey_hex` is the recipient's ML-KEM-768 public key (1184 bytes → 2368 hex chars). Send the returned `ciphertext` to the recipient; both sides end up with the same `shared_secret`. + +#### `decapsulate` (ML-KEM-768) + +```json +{ "id": "6", "method": "decapsulate", "params": [ "<ciphertext_hex>", { "algorithm": "ml-kem-768", "index": 0 } ] } +``` + +Response: +```json +{ "id": "6", "result": "{\"shared_secret\":\"<hex>\",\"algorithm\":\"ml-kem-768\"}" } +``` + +#### `derive_shared_secret` (X25519) + +```json +{ "id": "7", "method": "derive_shared_secret", "params": [ "<peer_pubkey_hex>", { "algorithm": "x25519", "index": 0 } ] } +``` + +Response: +```json +{ "id": "7", "result": "{\"shared_secret\":\"<hex>\",\"algorithm\":\"x25519\"}" } +``` + +`peer_pubkey_hex` is the peer's 32-byte X25519 public key (64 hex chars). Feed the returned `shared_secret` into your own symmetric cipher (e.g. AES-GCM, ChaCha20-Poly1305). + +#### `encrypt` / `decrypt` (OTP) + +```json +{ "id": "8", "method": "encrypt", "params": [ "<plaintext_base64>", { "algorithm": "otp", "encoding": "ascii" } ] } +{ "id": "9", "method": "decrypt", "params": [ "<ciphertext>", { "algorithm": "otp", "encoding": "ascii" } ] } +``` + +`encoding` is `"ascii"` (ASCII-armored, default) or `"binary"` (base64-encoded raw `.otp` blob). If omitted on `decrypt`, auto-detection by magic bytes is used. + +`encrypt` response: +```json +{ + "id": "8", + "result": "{\"ciphertext\":\"<ascii-armor-or-base64-blob>\",\"encoding\":\"ascii\",\"pad_chksum\":\"<64 hex>\",\"pad_offset_before\":288,\"pad_offset_after\":416}" +} +``` + +`decrypt` response: +```json +{ "id": "9", "result": "{\"plaintext\":\"<base64>\",\"pad_chksum\":\"<64 hex>\"}" } +``` + +If no pad is bound at startup, the error is `-32601` `otp_pad_not_bound`. + +#### `nostr_get_public_key` + +```json +{ "id": "10", "method": "nostr_get_public_key", "params": [ { "nostr_index": 0 } ] } +``` + +Response (default): a plain 64-hex-char secp256k1 public key string. +Response with `{"format":"structured"}` in options: `{"algorithm":"secp256k1","public_key":"<hex>","key_id":"<16 hex>"}`. + +#### `nostr_sign_event` + +Serializes the event to canonical form (`[0, pubkey, created_at, kind, tags, content]`), SHA-256 hashes it to produce the event `id`, signs the hash with BIP-340 Schnorr, and returns the complete signed event. + +```json +{ "id": "11", "method": "nostr_sign_event", "params": [ "<event_json>", { "nostr_index": 0 } ] } +``` + +`<event_json>` is the unsigned event object: +```json +{ "pubkey": "...", "created_at": 1234567890, "kind": 1, "tags": [], "content": "hello" } +``` + +Response: the signed event JSON string, with `id` and `sig` populated. + +#### `nostr_mine_event` + +Mines NIP-13 proof-of-work (adds a `nonce` tag) and signs the event in one step. Mining runs in a detached thread so the signer stays responsive. + +```json +{ + "id": "12", + "method": "nostr_mine_event", + "params": [ "<event_json>", { "difficulty": 20, "threads": 4, "timeout_sec": 30, "nostr_index": 0 } ] +} +``` + +| Option | Required | Default | Meaning | +|---------------|-------------------------------|---------|---------------------------------------------------------------| +| `difficulty` | one of `difficulty`/`timeout` | 0 | Target leading zero bits. Stops early if reached. | +| `timeout_sec` | one of `difficulty`/`timeout` | 600 | Time budget in seconds. Always returns the best event found. | +| `threads` | no | 1 | Mining threads (clamped to 1..32). | + +At least one of `difficulty` or `timeout_sec` must be specified. If both are given, mining stops when **either** condition is met. Timeout is never an error — the best event found is always returned. + +Response: +```json +{ + "id": "12", + "result": "{\"event\":\"<signed event JSON with nonce tag>\",\"achieved_difficulty\":18,\"target_difficulty\":20,\"target_reached\":false,\"elapsed_sec\":30,\"attempts\":4523456}" +} +``` + +Errors: +- `1007` `no_termination_condition` — neither `difficulty` nor `timeout_sec` given. +- `1008` `mining_failed` — internal mining error. + +#### `nostr_nip04_encrypt` / `nostr_nip04_decrypt` + +NIP-04 encryption (deprecated in Nostr but still widely used): ECDH + AES-256-CBC, base64 payload. + +```json +{ "id": "13", "method": "nostr_nip04_encrypt", "params": [ "<peer_pubkey_hex>", "<plaintext>", { "nostr_index": 0 } ] } +{ "id": "14", "method": "nostr_nip04_decrypt", "params": [ "<peer_pubkey_hex>", "<ciphertext>", { "nostr_index": 0 } ] } +``` + +`encrypt` returns the NIP-04 ciphertext string; `decrypt` returns the plaintext string. + +#### `nostr_nip44_encrypt` / `nostr_nip44_decrypt` + +NIP-44 encryption (current Nostr standard): ECDH + HKDF + ChaCha20-Poly1305 + specific payload format. + +```json +{ "id": "15", "method": "nostr_nip44_encrypt", "params": [ "<peer_pubkey_hex>", "<plaintext>", { "nostr_index": 0 } ] } +{ "id": "16", "method": "nostr_nip44_decrypt", "params": [ "<peer_pubkey_hex>", "<ciphertext>", { "nostr_index": 0 } ] } +``` + +`encrypt` returns the NIP-44 ciphertext string; `decrypt` returns the plaintext string. + +### 4.6 Role-based selectors (Nostr verbs) + +The `nostr_*` verbs select a secp256k1 NIP-06 key via the options object. Supported selectors: + +| Selector | Meaning | +|----------------|--------------------------------------------------| +| `nostr_index` | NIP-06 index `n` → path `m/44'/1237'/<n>'/0/0` | +| `role` | Name of a pre-registered role entry | +| `role_path` | Full BIP-44 derivation path (must match a registered role) | + +Selector resolution order: `role` → `nostr_index` → `role_path` → default role `main`. Conflicting selectors are rejected with `ambiguous_role_selector` (1001). The role's `(purpose, curve)` must be `(nostr, secp256k1)` — any other combination is rejected with `purpose_mismatch` (1004) or `curve_mismatch` (1005). + +### 4.7 Pre-approval + +Pre-approval entries skip the interactive prompt for matching requests. They are configured at startup with `--preapprove`. + +Algorithm-based: +```bash nsigner --preapprove caller=uid:1000,algorithm=ed25519,index=0-4,verb=sign,verify nsigner --preapprove caller=uid:1000,algorithm=ml-kem-768,index=0,verb=decapsulate ``` -### 4c.6 Enforcement matrix - -| Verb | Valid algorithms | -|---|---| -| `sign` / `verify` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | -| `encapsulate` / `decapsulate` | ml-kem-768 | -| `derive_shared_secret` | x25519 | -| `get_public_key` | all algorithms | -| `sign_event` / `nip44_*` / `nip04_*` / `mine_event` | secp256k1 (Nostr protocol) | - -## 5. Wire contract (JSON-RPC) - -> **The full, authoritative API reference now lives in [`api.md`](api.md).** The summary below is kept for orientation; for request/response shapes, error codes, and worked examples, consult [`api.md`](api.md). - -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" }] } -{ "id": "5", "method": "mine_event", "params": ["<event_json>", { "difficulty": 20, "threads": 4, "timeout_sec": 30, "nostr_index": 0 }] } +Nostr (role-based): +```bash +nsigner --preapprove caller=uid:1000,nostr_index=0,verb=nostr_sign_event,nostr_get_public_key ``` -Implemented signer verbs in this build: +A `*` wildcard matches any caller, role, or verb. Index ranges use `min-max` syntax. Unmatched requests fall through to the default policy (prompt for same-uid, deny for others). -**Nostr verbs** (secp256k1 only, use `role` or `nostr_index` selector): +## 5. Transports -- `get_public_key` -- `sign_event` -- `nip04_encrypt` / `nip04_decrypt` -- `nip44_encrypt` / `nip44_decrypt` -- `mine_event` — add NIP-13 proof-of-work and sign (see below) +The API is transport-independent. The same JSON request works over every transport; only the framing differs. -**Algorithm-based verbs** (use `algorithm` + `index` selector, no role needed): +| Transport | `--listen` flag | Framing | Caller identity | +|-----------|--------------------------------|------------------------------------------|----------------------------| +| Unix socket (abstract) | `unix` (default on desktop) | Length-prefixed framed JSON | `SO_PEERCRED` → `uid:<n>` | +| stdio | `stdio` | One framed request/response over stdin/stdout | inherited uid | +| qrexec | `qrexec` | Same as stdio; caller from `QREXEC_REMOTE_DOMAIN` | `qubes:<vm>` | +| FIPS/TCP | `tcp:[host]:port` | Length-prefixed framed JSON | (transport-defined) | +| HTTP | `http:host:port` | Standard HTTP POST, JSON body, no custom framing. CORS enabled. | (transport-defined) | -| Verb | Valid algorithms | Description | -|---|---|---| -| `sign` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | Sign arbitrary bytes. Returns `{"signature":"<hex>","algorithm":"<alg>","key_id":"<16hex>"}`. | -| `verify` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | Verify a signature. Returns `{"valid":true/false}`. | -| `encapsulate` | ml-kem-768 | Encapsulate against a peer's ML-KEM-768 public key. Returns `{"ciphertext":"<hex>","shared_secret":"<hex>","algorithm":"ml-kem-768"}`. | -| `decapsulate` | ml-kem-768 | Decapsulate a ciphertext using the derived ML-KEM-768 private key. Returns `{"shared_secret":"<hex>","algorithm":"ml-kem-768"}`. | -| `derive_shared_secret` | x25519 | ECDH key agreement. Returns `{"shared_secret":"<hex>","algorithm":"x25519"}`. | -| `get_public_key` | all algorithms | Returns the public key for the specified algorithm+index. | +### 5.1 HTTP examples -**General encryption verbs** (use `curve` parameter to select encryption method): - -| Verb | `curve` value | Description | -|---|---|---| -| `encrypt` | `otp` | One-time pad encryption (requires `--otp-pad-dir` + `--otp-pad`). Returns ASCII-armored or binary ciphertext. | -| `decrypt` | `otp` | One-time pad decryption. Returns plaintext (base64). | -| `encrypt` | `secp256k1` | NIP-44 (default) or NIP-04 encryption. Params: `[peer_pubkey, message, {nip_version: 4\|44}]`. | -| `decrypt` | `secp256k1` | NIP-44 (default) or NIP-04 decryption. Params: `[peer_pubkey, ciphertext, {nip_version: 4\|44}]`. | -| `encrypt` | `x25519` | ECDH + symmetric encryption (not yet implemented — use `derive_shared_secret` + your own cipher). | -| `encrypt` | `ml-kem-768` | KEM-based hybrid encryption (not yet implemented — use `encapsulate`/`decapsulate` + your own cipher). | - -**OTP verb aliases** (also available, same as `encrypt`/`decrypt` with `curve: "otp"`): - -| Verb | Description | -|---|---| -| `otp_encrypt` | Encrypt plaintext (base64) with the bound OTP pad. Returns ASCII-armored or binary ciphertext. | -| `otp_decrypt` | Decrypt ciphertext with the bound OTP pad. Returns plaintext (base64). | - -See [`plans/otp_nostr_integration.md`](plans/otp_nostr_integration.md) for the full OTP design. - -Example `sign` request (algorithm-based): - -```jsonc -{ "id": "6", "method": "sign", "params": ["<message_hex>", { "algorithm": "ed25519", "index": 0 }] } -``` - -Example `otp_encrypt` request: - -```jsonc -{ "id": "7", "method": "otp_encrypt", "params": ["<plaintext_base64>", { "encoding": "ascii" }] } -``` - -### `mine_event` — NIP-13 Proof-of-Work - -Mines proof-of-work (adds a `nonce` tag per NIP-13) and signs the event in one step. The mining runs in a detached thread so the server stays responsive. - -**Parameters (in options object):** - -| Option | Required | Default | Description | -|--------|----------|---------|-------------| -| `difficulty` | One of difficulty/timeout | 0 (no target) | Target leading zero bits. Stops early if reached. | -| `timeout_sec` | One of difficulty/timeout | 600 (safety) | Time budget in seconds. Always returns best result found. | -| `threads` | No | 1 | Number of mining threads (max 32). | - -At least one of `difficulty` or `timeout_sec` must be specified. If both are given, mining stops when either condition is met. The response always includes the best event found — timeout is not an error. - -**Response format:** - -```json -{ - "id": "5", - "result": { - "event": "<signed event JSON with nonce tag>", - "achieved_difficulty": 18, - "target_difficulty": 20, - "target_reached": false, - "elapsed_sec": 30, - "attempts": 4523456 - } -} -``` - -**Error codes:** - -- `1007` — `no_termination_condition` (neither difficulty nor timeout_sec specified) -- `1008` — `mining_failed` (internal error) - -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 (role-based API) - -Selector resolution chooses *which* role. Enforcement decides *whether the requested method is valid* for that role. - -**Note:** The algorithm-based API (§4c) bypasses purpose/curve enforcement entirely — the caller specifies the algorithm directly, and the dispatcher derives the key on demand. Purpose values are only used by the role-based API for enforcement. - -### 6.1 Purpose values (role-based API only) - -| Purpose | Description | -|---|---| -| `nostr` | Nostr identities (secp256k1, NIP-06) | -| `bitcoin` | Bitcoin key trees (secp256k1, BIP-44) | -| `ssh` | SSH signing keys (ed25519) | -| `age` | age-style encryption identities (x25519) | -| `fips` | FIPS mesh/service identities | -| `pq-sig` | Post-quantum signatures (ML-DSA-65, SLH-DSA-128s) | -| `pq-kem` | Post-quantum key encapsulation (ML-KEM-768) | - -### 6.2 Curve values - -| Curve | Algorithms | -|---|---| -| `secp256k1` | ECDSA/Schnorr for Nostr, Bitcoin | -| `ed25519` | Ed25519 for SSH signatures | -| `x25519` | X25519 for key agreement (age) | -| `ml-dsa-65` | ML-DSA-65 (FIPS 204, lattice-based PQ signatures) | -| `slh-dsa-128s` | SLH-DSA-128s (FIPS 205, hash-based PQ signatures) | -| `ml-kem-768` | ML-KEM-768 (FIPS 203, lattice-based PQ KEM) | - -### 6.3 Enforcement matrix (role-based API) - -| Verb | Required purpose | Required curve | -|---|---|---| -| `sign_event` | `nostr` | `secp256k1` | -| `mine_event` | `nostr` | `secp256k1` | -| `nip04_encrypt` / `nip04_decrypt` | `nostr` | `secp256k1` | -| `nip44_encrypt` / `nip44_decrypt` | `nostr` | `secp256k1` | -| `get_public_key` | any | any (must match role's declared curve) | -| `sign_data` / `ssh_sign` | `ssh` or `pq-sig` | `ed25519`, `ml-dsa-65`, or `slh-dsa-128s` | -| `verify_signature` | `ssh` or `pq-sig` | `ed25519`, `ml-dsa-65`, or `slh-dsa-128s` | -| `kem_encapsulate` / `kem_decapsulate` | `pq-kem` | `ml-kem-768` | - -The algorithm-based API (§4c) does not use this matrix — the caller specifies the algorithm directly, and enforcement is based on the verb+algorithm combination (see [§4c.6](#4c6-enforcement-matrix)). - -Example (role-based): - -- `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. Fail-closed: any unlisted `(verb, purpose, curve)` combination is rejected. - -## 7. Transport - -### 7.1 Linux desktop: abstract namespace Unix socket - -Primary local transport is AF_UNIX abstract namespace. - -Each running `nsigner` process binds to a unique abstract name of the form `@nsigner_<word1>_<word2>`, where the two words are picked at random from the BIP-39 English wordlist at startup (e.g. `@nsigner_hairy_dog`). This lets multiple signers coexist on one host. - -Properties: - -- no pathname in filesystem -- endpoint lifetime bound to process/kernel namespace -- no stale socket files -- caller identity via peer credentials (`SO_PEERCRED`) -- per-launch random name avoids collisions between concurrent instances and leaks no seed-derived identifier - -Naming rules: - -- Default: random pick at startup, displayed in the Connections section and status line of the running display. -- Override: `--socket-name <name>` (alias: `--name <name>` / `-n <name>`) forces a specific name (useful for scripts and tests). -- Collision: if the chosen random name is already bound by another process, `nsigner` retries with a fresh pair up to 8 times before erroring out with a hint to use an explicit name override. - -Discovery: - -- `nsigner list` enumerates currently bound `nsigner_*` abstract sockets by reading `/proc/net/unix`. -- `nsigner --listen stdio` runs one framed JSON-RPC request/response over stdin/stdout. -- `nsigner --listen qrexec` is the same stdio framing mode, but caller identity can be derived from `QREXEC_REMOTE_DOMAIN` (displayed as `qubes:<source-vm>`). -- `nsigner --listen tcp:IPv4:PORT` or `tcp:[IPv6]:PORT` enables FIPS/TCP listening for non-AF_UNIX clients (for example `tcp:127.0.0.1:11111`, `tcp:[::]:11111`, or `tcp:[fd00::1234]:11111`). Uses framed JSON protocol (not HTTP). -- `nsigner --listen http:HOST:PORT` enables HTTP listening for curl-friendly access (for example `http:127.0.0.1:11111`). Uses standard HTTP POST with JSON body — no custom framing. CORS headers included for browser access. Defaults to localhost; pass `http:0.0.0.0:PORT` to expose externally. -- `nsigner bridge --to <socket-name>` is a stateless relay for Qubes qrexec: reads one framed request from stdin, forwards it to a persistent signer's abstract unix socket, and relays the response to stdout. Used as the `qubes.NsignerRpc` service entrypoint. See [§8.3](#83-qubes-os-qube) and [`plans/qrexec_persistent_bridge.md`](plans/qrexec_persistent_bridge.md). -- `--bridge-source-trusted` (unix listener only): marks the socket as a trusted bridge endpoint. Each connection sends a framed `{"qrexec_source":"<vm>"}` preamble before the request, and the caller identity is composed as `qubes:<vm>` — matching the native qrexec identity path. This enables a persistent signer (mnemonic in mlock'd RAM) to receive qrexec-routed requests without spawning a fresh process per call. - -#### curl examples (HTTP mode) - -Start the signer in HTTP mode: +Start the signer: ```bash nsigner --listen http:127.0.0.1:11111 --allow-all ``` @@ -591,69 +493,62 @@ nsigner --listen http:127.0.0.1:11111 --allow-all Get a public key: ```bash curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \ - -d '{"id":"1","method":"get_public_key","params":[{"role":"main"}]}' + -d '{"id":"1","method":"get_public_key","params":[{"algorithm":"ed25519","index":0}]}' ``` Sign a Nostr event: ```bash curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \ - -d '{"id":"1","method":"sign_event","params":[{"pubkey":"...","created_at":1234567890,"kind":1,"tags":[],"content":"hello"}]}' + -d '{"id":"1","method":"nostr_sign_event","params":[{"pubkey":"...","created_at":1234567890,"kind":1,"tags":[],"content":"hello"},{"nostr_index":0}]}' ``` -General encrypt (OTP, requires `--otp-pad-dir` and `--otp-pad`): +OTP encrypt: ```bash curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \ - -d '{"id":"1","method":"encrypt","params":["SGVsbG8sIE9UUCB3b3JsZCE=",{"curve":"otp","encoding":"ascii"}]}' + -d '{"id":"1","method":"encrypt","params":["SGVsbG8sIE9UUCB3b3JsZCE=",{"algorithm":"otp","encoding":"ascii"}]}' ``` -General encrypt (NIP-44, secp256k1): -```bash -curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \ - -d '{"id":"1","method":"encrypt","params":["<peer_pubkey_hex>","Hello!",{"role":"main"}]}' -``` - -General decrypt (OTP): -```bash -curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \ - -d '{"id":"1","method":"decrypt","params":["-----BEGIN OTP MESSAGE-----...",{"curve":"otp","encoding":"ascii"}]}' -``` - -OTP verb aliases (same as encrypt/decrypt with curve=otp): -```bash -curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \ - -d '{"id":"1","method":"otp_encrypt","params":["SGVsbG8sIE9UUCB3b3JsZCE=",{"encoding":"ascii"}]}' -``` - -#### Unix socket examples (framed mode) +### 5.2 Unix socket examples (framed mode) ```bash -# Get public key -nsigner --socket-name nsigner client '{"id":"1","method":"get_public_key","params":[{"role":"main"}]}' +# get_public_key +nsigner --socket-name nsigner client \ + '{"id":"1","method":"get_public_key","params":[{"algorithm":"ed25519","index":0}]}' -# Sign event -nsigner --socket-name nsigner client '{"id":"1","method":"sign_event","params":[{"pubkey":"...","created_at":1234567890,"kind":1,"tags":[],"content":"hello"}]}' +# Sign a Nostr event +nsigner --socket-name nsigner client \ + '{"id":"2","method":"nostr_sign_event","params":[{"pubkey":"...","created_at":1234567890,"kind":1,"tags":[],"content":"hello"},{"nostr_index":0}]}' -# Encrypt (OTP) -nsigner --socket-name nsigner client '{"id":"1","method":"encrypt","params":["SGVsbG8=",{"curve":"otp","encoding":"ascii"}]}' - -# Encrypt (NIP-44) -nsigner --socket-name nsigner client '{"id":"1","method":"encrypt","params":["<peer_pubkey>","Hello!",{"role":"main"}]}' +# ed25519 sign +nsigner --socket-name nsigner client \ + '{"id":"3","method":"sign","params":["68656c6c6f",{"algorithm":"ed25519","index":0}]}' ``` -### 7.2 ESP32 MCU: TinyUSB composite (CDC + WebUSB) +### 5.3 Linux desktop: abstract namespace Unix socket -On Feather S3 TFT, MCU targets run the same core signer modules behind a TinyUSB composite transport: +Primary local transport is AF_UNIX abstract namespace. Each running `nsigner` process binds to a unique abstract name of the form `@nsigner_<word1>_<word2>`, where the two words are picked at random from the BIP-39 English wordlist at startup (e.g. `@nsigner_hairy_dog`). This lets multiple signers coexist on one host. -- CDC-ACM framed JSON-RPC for serial tooling (`/dev/ttyACM*`) -- Vendor/WebUSB framed JSON-RPC for browser tooling +Properties: no pathname in filesystem; endpoint lifetime bound to process/kernel namespace; no stale socket files; caller identity via peer credentials (`SO_PEERCRED`); per-launch random name avoids collisions and leaks no seed-derived identifier. -Core signer logic stays the same; only transport/UI bindings change. +Naming rules: +- Default: random pick at startup, displayed in the status line. +- Override: `--socket-name <name>` (alias: `--name <name>` / `-n <name>`) forces a specific name. +- Collision: if the chosen random name is already bound, `nsigner` retries with a fresh pair up to 8 times before erroring out. -### 7.3 NIP-46 relay flow (both platforms) +Discovery: +- `nsigner list` enumerates currently bound `nsigner_*` abstract sockets by reading `/proc/net/unix`. +- `nsigner --listen stdio` runs one framed JSON-RPC request/response over stdin/stdout. +- `nsigner --listen qrexec` is the same stdio framing, but caller identity comes from `QREXEC_REMOTE_DOMAIN` (displayed as `qubes:<source-vm>`). +- `nsigner --listen tcp:IPv4:PORT` or `tcp:[IPv6]:PORT` enables FIPS/TCP listening (framed JSON, not HTTP). +- `nsigner --listen http:HOST:PORT` enables HTTP listening for curl-friendly access. CORS headers included for browser access. Defaults to localhost; pass `http:0.0.0.0:PORT` to expose externally. +- `nsigner bridge --to <socket-name>` is a stateless relay for Qubes qrexec: reads one framed request from stdin, forwards it to a persistent signer's abstract unix socket, and relays the response to stdout. Used as the `qubes.NsignerRpc` service entrypoint. See [`plans/qrexec_persistent_bridge.md`](plans/qrexec_persistent_bridge.md). +- `--bridge-source-trusted` (unix listener only): marks the socket as a trusted bridge endpoint. Each connection sends a framed `{"qrexec_source":"<vm>"}` preamble before the request, and the caller identity is composed as `qubes:<vm>`. -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. +### 5.4 ESP32 MCU: TinyUSB composite (CDC + WebUSB) -### 7.4 Caller verification +On Feather S3 TFT, MCU targets run the same core signer modules behind a TinyUSB composite transport: CDC-ACM framed JSON-RPC for serial tooling (`/dev/ttyACM*`) and Vendor/WebUSB framed JSON-RPC for browser tooling. Core signer logic stays the same; only transport/UI bindings change. + +### 5.5 Caller verification Every transport must provide concrete caller identity before policy evaluation. @@ -663,164 +558,131 @@ Every transport must provide concrete caller identity before policy evaluation. Identity verification and interactive approval are separate layers. Passing identity checks does not bypass prompt requirements. -## 8. Platform targets +## 6. Platform targets -### 8.1 Linux desktop (primary) +### 6.1 Linux desktop (primary) Primary deployment is a local, foreground terminal program with abstract namespace socket transport. -### 8.2 ESP32 / MCU +### 6.2 ESP32 / MCU MCU target reuses mnemonic/role/selector/enforcement/dispatcher core and swaps transport/UI for constrained hardware. The Feather path currently uses TinyUSB composite USB (CDC + WebUSB) plus TFT/buttons for attended approvals. -### 8.3 Qubes OS qube +### 6.3 Qubes OS qube Qubes deployment runs `n_signer` in a dedicated signer qube (e.g. `nostr_signer`) as a foreground process under explicit user session control. The mnemonic lives only in mlock'd RAM in that qube — a compromised agent in a caller qube cannot read it (hypervisor-enforced memory isolation). Three transport paths are supported: -**FIPS/TCP** — the signer listens on `tcp:[::]:11111` and FIPS carries traffic between qubes as an IPv6 mesh substrate. Uses framed JSON protocol. See [`documents/FIPS_DEPLOYMENT.md`](documents/FIPS_DEPLOYMENT.md). +**FIPS/TCP** — the signer listens on `tcp:[::]:11111` and FIPS carries traffic between qubes as an IPv6 mesh substrate. See [`documents/FIPS_DEPLOYMENT.md`](documents/FIPS_DEPLOYMENT.md). -**HTTP** — the signer listens on `http:127.0.0.1:11111` for curl-friendly access within the same qube. Uses standard HTTP POST with JSON body. No auth envelopes required (relies on localhost binding + policy/approval prompts). Add `--listen http:127.0.0.1:11111` or select option 4 in the interactive transport menu. +**HTTP** — the signer listens on `http:127.0.0.1:11111` for curl-friendly access within the same qube. No auth envelopes required (relies on localhost binding + policy/approval prompts). -**Qubes qrexec bridge** (recommended for no-network deployments) — a persistent signer listens on an abstract unix socket, and a stateless `nsigner bridge` relay (the `qubes.NsignerRpc` qrexec service) forwards one request per qrexec invocation. No network, no FIPS — pure intra-host IPC. Caller identity is `qubes:<source-vm>` (from `QREXEC_REMOTE_DOMAIN`), relayed via a trusted preamble. See [`plans/qrexec_persistent_bridge.md`](plans/qrexec_persistent_bridge.md) for the full design. +**Qubes qrexec bridge** (recommended for no-network deployments) — a persistent signer listens on an abstract unix socket, and a stateless `nsigner bridge` relay (the `qubes.NsignerRpc` qrexec service) forwards one request per qrexec invocation. No network, no FIPS — pure intra-host IPC. Caller identity is `qubes:<source-vm>`. See [`plans/qrexec_persistent_bridge.md`](plans/qrexec_persistent_bridge.md) for the full design. #### Qrexec bridge setup **In the signer qube** (`nostr_signer`): - ```bash -# Install nsigner and the qrexec service bash setup_signer_qube.sh # from packaging/qubes/ - -# Start the persistent signer (mnemonic entered at terminal, in mlock'd RAM) ~/.local/bin/nsigner --listen unix --socket-name nsigner --bridge-source-trusted ``` **In dom0**: - ```bash -# Install policy and tag the signer qube bash setup_dom0.sh nostr_signer # from packaging/qubes/ ``` The dom0 policy allows trusted caller qubes without a popup (memory isolation is the real security boundary) and asks for confirmation from any other qube. The signer's own approval prompt at the `nostr_signer` terminal is the operation-level gate. **From a caller qube**: - ```bash -# JavaScript example (uses qrexec-client-vm, no auth envelope needed) node examples/n_signer_qube_example_qrexec.js nostr_signer ``` Setup scripts and policy are in [`packaging/qubes/`](packaging/qubes/). See also [`documents/QUBES_OS.md`](documents/QUBES_OS.md) and [`documents/qubes_client_examples.md`](documents/qubes_client_examples.md). -## 9. Usage +## 7. Usage -### 9.1 Run the program +### 7.1 Run the program ```bash nsigner ``` -Program starts in attached foreground mode and prompts for mnemonic. After mnemonic acceptance, the running status display shows the randomly assigned signer name and its abstract socket address in the Connections section and the bottom status line (see [§3.2](#32-running-phase-status-display--signer)). +Program starts in attached foreground mode and prompts for mnemonic. After mnemonic acceptance, the running status display shows the randomly assigned signer name and its abstract socket address. To force a specific socket name (e.g. for scripted clients): - ```bash nsigner --name my_test_signer ``` -Qubes/qrexec service mode (single framed request over stdin/stdout): - +Other transport modes: ```bash -nsigner --listen qrexec +nsigner --listen qrexec # Qubes qrexec (single framed request over stdin/stdout) +nsigner --listen stdio # Generic stdio (single framed request over stdin/stdout) +nsigner --listen tcp:[::]:11111 # FIPS/TCP (framed JSON, no TUI) +nsigner --listen http:127.0.0.1:11111 # HTTP (curl-friendly, no TUI) ``` -Generic stdio transport mode (single framed request over stdin/stdout): - -```bash -nsigner --listen stdio -``` - -FIPS/TCP transport mode (framed JSON, no TUI; serves requests until terminated): - -```bash -nsigner --listen tcp:[::]:11111 -``` - -HTTP transport mode (curl-friendly, no TUI; serves requests until terminated): - -```bash -nsigner --listen http:127.0.0.1:11111 -``` - -With OTP pad bound (auto-detects pads on USB drives in interactive mode): - +With OTP pad bound: ```bash nsigner --listen http:127.0.0.1:11111 --otp-pad-dir /media/user/Music/pads --otp-pad 333e9902db839d9d --allow-all ``` -Qrexec bridge mode (stateless relay to a persistent signer's unix socket; used as the `qubes.NsignerRpc` service): - +Qrexec bridge mode (stateless relay to a persistent signer's unix socket): ```bash nsigner bridge --to nsigner ``` Persistent signer for qrexec bridge (unix listener with trusted source-qube preamble): - ```bash nsigner --listen unix --socket-name nsigner --bridge-source-trusted ``` -### 9.2 Send a request (client mode) +### 7.2 Send a request (client mode) From another terminal, target the signer by its socket name: - ```bash -nsigner --socket-name nsigner_hairy_dog client '{"id":"1","method":"get_public_key","params":[]}' +nsigner --socket-name nsigner_hairy_dog client '{"id":"1","method":"nostr_get_public_key","params":[]}' ``` If only one signer is running you can omit the override and the client will use the default discovery rule. Example signing request: - ```bash -nsigner -n nsigner_hairy_dog client '{"id":"2","method":"sign_event","params":["<event_json>",{"role":"main"}]}' +nsigner -n nsigner_hairy_dog client '{"id":"2","method":"nostr_sign_event","params":["<event_json>",{"role":"main"}]}' ``` -### 9.3 List running signers +### 7.3 List running signers ```bash nsigner list ``` Prints the abstract socket names of any currently running `nsigner` instances, e.g.: - ```text @nsigner_hairy_dog @nsigner_brave_canyon ``` -### 9.4 Example session +### 7.4 Example session Terminal A: - ```text $ nsigner [unlock] enter mnemonic: System is ready and waiting for connections on @nsigner_hairy_dog. -[prompt] caller=uid:1000 method=sign_event role=main -> allow? (y/n) +[prompt] caller=uid:1000 method=nostr_sign_event role=main -> allow? (y/n) ``` Terminal B: - ```text -$ nsigner --socket-name nsigner_hairy_dog client '{"id":"2","method":"sign_event","params":["<event_json>",{"role":"main"}]}' +$ nsigner --socket-name nsigner_hairy_dog client '{"id":"2","method":"nostr_sign_event","params":["<event_json>",{"role":"main"}]}' {"id":"2","result":"<signed_event_json>"} ``` -## 10. Build and versioning +## 8. Build and versioning Build outputs a single executable artifact. @@ -832,16 +694,13 @@ Build outputs a single executable artifact. - [`src/main.c`](src/main.c): version macros (`NSIGNER_VERSION*`) Local dev build: - ```bash make dev ./build/nsigner --version ``` Static build: - ```bash ./build_static.sh ./build/nsigner_static_x86_64 --version ``` - diff --git a/api.md b/api.md index 956cc38..a6ff15b 100644 --- a/api.md +++ b/api.md @@ -1,382 +1,15 @@ # n_signer API -`n_signer` exposes a JSON-RPC 2.0-style request/response protocol. Every request is a single JSON object; every response is a single JSON object. This document is the complete, authoritative description of the API. +The complete, authoritative API reference is now in [`README.md`](README.md) §4 (API). -For the security model, transports, and operational behavior, see [`README.md`](README.md). For the migration plan from the legacy verb names, see [`plans/legacy_verb_aliases.md`](plans/legacy_verb_aliases.md). +It covers: ---- +- **§4.1 Request format** — JSON-RPC 2.0-style request shape. +- **§4.2 Response format** — success/error shapes and the full error-code table. +- **§4.3 Verbs** — the verb table (positional params + options), the `scheme` option for secp256k1, and the enforcement matrix. +- **§4.4 Algorithms** — the algorithm table (secp256k1, ed25519, x25519, ml-dsa-65, slh-dsa-128s, ml-kem-768, otp), derivation paths, key sizes, and the OTP one-time-pad model. +- **§4.5 Examples** — worked request/response examples for every verb. +- **§4.6 Role-based selectors** — `nostr_index` / `role` / `role_path` for the `nostr_*` verbs. +- **§4.7 Pre-approval** — `--preapprove` syntax for algorithm-based and Nostr verbs. -## 1. API format - -### 1.1 Request - -```json -{ "id": "<string>", "method": "<verb>", "params": [ <arg0>, <arg1>, ..., { <options> } ] } -``` - -- `id` — caller-supplied string echoed verbatim in the response. Used to match requests to responses. -- `method` — the verb name (see [§2](#2-verbs)). -- `params` — a JSON array. Positional arguments come first; the **last array element** is conventionally an options object. The options object is optional for most verbs. - -### 1.2 Response (success) - -```json -{ "id": "<string>", "result": <value> } -``` - -`result` is a JSON string. For structured verbs the string is itself a serialized JSON object — clients should `JSON.parse` it. - -### 1.3 Response (error) - -```json -{ "id": "<string>", "error": { "code": <int>, "message": "<string>" } } -``` - -### 1.4 Error codes - -| Code | Message | Meaning | -|-------|-------------------------------|--------------------------------------------------------------------| -| -32700| `parse_error` | Request was not valid JSON. | -| -32600| `invalid_request` | Missing `id`, `method`, or `params`, or `params` is not an array. | -| -32601| `method_not_found` | Unknown verb, or verb not valid for the selected algorithm. | -| -32602| `invalid_params` | Malformed arguments (bad hex, wrong length, missing field, etc.). | -| 1001 | `ambiguous_role_selector` | More than one role selector was supplied. | -| 1002 | `unknown_role` | No role matched the selector. | -| 1003 | `no_default_role` | No selector given and no `main` role exists. | -| 1004 | `purpose_mismatch` | Role's purpose is not valid for this verb. | -| 1005 | `curve_mismatch` | Role's curve is not valid for this verb. | -| 1006 | `mnemonic_not_loaded` | No mnemonic is loaded in the signer. | -| 1007 | `no_termination_condition` | `nostr_mine_event` called without `difficulty` or `timeout_sec`. | -| 1008 | `mining_failed` | Internal error during proof-of-work mining. | -| 1009 | `not_yet_implemented` | Verb+algorithm combination is reserved but not yet implemented. | -| 1010 | `algorithm_not_supported_for_verb` | The `algorithm` value is not valid for this verb. | - ---- - -## 2. Verbs - -All verbs take their arguments as positional `params` and their options in a trailing options object. Most verbs select a key via the `algorithm` + `index` options (see [§3](#3-algorithms)). The `nostr_*` verbs select a secp256k1 NIP-06 key via `nostr_index` and implement Nostr-protocol-specific serialization on top of the raw crypto. - -| Verb | Algorithms | Positional params | Options | -|-------------------------|-----------------------------------------------|----------------------------------|----------------------------------| -| `get_public_key` | all key-deriving algorithms | — | `algorithm`, `index` | -| `sign` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | `<message_hex>` | `algorithm`, `index`, `scheme`* | -| `verify` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | `<message_hex>`, `<signature_hex>` | `algorithm`, `index`, `scheme`* | -| `encapsulate` | ml-kem-768 | `<peer_pubkey_hex>` | `algorithm` | -| `decapsulate` | ml-kem-768 | `<ciphertext_hex>` | `algorithm`, `index` | -| `derive_shared_secret` | x25519 | `<peer_pubkey_hex>` | `algorithm`, `index` | -| `encrypt` | otp | `<plaintext_base64>` | `algorithm`, `encoding` | -| `decrypt` | otp | `<ciphertext>` | `algorithm`, `encoding` | -| `nostr_get_public_key` | secp256k1 (NIP-06) | — | `nostr_index`, `format` | -| `nostr_sign_event` | secp256k1 (NIP-06) | `<event_json>` | `nostr_index` | -| `nostr_mine_event` | secp256k1 (NIP-06) | `<event_json>` | `nostr_index`, `difficulty`, `timeout_sec`, `threads` | -| `nostr_nip04_encrypt` | secp256k1 (NIP-06) | `<peer_pubkey_hex>`, `<plaintext>` | `nostr_index` | -| `nostr_nip04_decrypt` | secp256k1 (NIP-06) | `<peer_pubkey_hex>`, `<ciphertext>` | `nostr_index` | -| `nostr_nip44_encrypt` | secp256k1 (NIP-06) | `<peer_pubkey_hex>`, `<plaintext>` | `nostr_index` | -| `nostr_nip44_decrypt` | secp256k1 (NIP-06) | `<peer_pubkey_hex>`, `<ciphertext>` | `nostr_index` | - -\* `scheme` is secp256k1-only: `"schnorr"` (default, BIP-340) or `"ecdsa"`. - -### 2.1 Enforcement matrix - -| Verb | Valid algorithms | -|----------------------------|-----------------------------------------------| -| `sign` / `verify` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | -| `encapsulate` / `decapsulate` | ml-kem-768 | -| `derive_shared_secret` | x25519 | -| `encrypt` / `decrypt` | otp | -| `get_public_key` | all key-deriving algorithms | -| `nostr_*` | secp256k1 (Nostr protocol) | - -Any unlisted `(verb, algorithm)` pair is rejected with `algorithm_not_supported_for_verb` (1010). - ---- - -## 3. Algorithms - -All keys derive deterministically from the loaded BIP-39 mnemonic. The caller selects an algorithm by name and a derivation `index` (an integer `<n>` substituted into the algorithm's derivation path). OTP is the exception — it does not derive a key, it consumes a bound one-time pad (see [§3.3](#33-otp)). - -### 3.1 Algorithm table - -| Algorithm | Key type | FIPS standard | Derivation path | Key sizes (priv / pub, bytes) | -|-----------------|-----------------|---------------|---------------------------------------|-------------------------------| -| `secp256k1` | Signature | — | `m/44'/1237'/<n>'/0/0` (NIP-06) | 32 / 32 | -| `ed25519` | Signature | — | `m/44'/102001'/<n>'/0/0'` (SLIP-0010) | 32 / 32 | -| `x25519` | Key agreement | — | `m/44'/102002'/<n>'/0/0'` (SLIP-0010) | 32 / 32 | -| `ml-dsa-65` | PQ signature | FIPS 204 | `m/44'/102003'/<n>'/0/0'` → DRBG | 4032 / 1952 | -| `slh-dsa-128s` | PQ signature | FIPS 205 | `m/44'/102004'/<n>'/0/0'` → DRBG | 64 / 32 | -| `ml-kem-768` | PQ KEM | FIPS 203 | `m/44'/102005'/<n>'/0/0'` → DRBG | 2400 / 1184 | -| `otp` | One-time pad | — | (no key — bound USB pad) | n/a | - -### 3.2 Key derivation - -- **secp256k1** uses standard BIP-32/NIP-06 derivation. The 32-byte path output is the private key scalar. -- **ed25519 / x25519** use SLIP-0010 HMAC-SHA512 derivation (all-hardened paths, as required by SLIP-0010 for ed25519). The 32-byte output is the private key. -- **PQ algorithms** (ML-DSA-65, SLH-DSA-128s, ML-KEM-768) use a two-stage approach: the mnemonic-derived 32-byte seed feeds a SHAKE-256 DRBG (NIST SP 800-90A style), which replaces PQClean's `randombytes()` callback during keygen. Same mnemonic, same index, same key pair every time. -- **otp** does not derive a key. A pad is bound at signer startup (`--otp-pad-dir` + `--otp-pad`); the pad offset advances monotonically across requests. - -### 3.3 OTP - -The `otp` algorithm is a stream-style one-time pad, not a key-derivation scheme. It is selected like any other algorithm via `{"algorithm":"otp"}` and works with the `encrypt` / `decrypt` verbs. One pad per session; the pad offset advances monotonically across requests and is reported in every response. - ---- - -## 4. Examples - -### 4.1 `get_public_key` - -```json -{ "id": "1", "method": "get_public_key", "params": [ { "algorithm": "ml-dsa-65", "index": 0 } ] } -``` - -Response: -```json -{ "id": "1", "result": "{\"algorithm\":\"ml-dsa-65\",\"public_key\":\"<hex>\",\"key_id\":\"<16 hex>\"}" } -``` - -`key_id` is the first 16 hex characters of the public key — a short display identifier. - -### 4.2 `sign` - -```json -{ "id": "2", "method": "sign", "params": [ "68656c6c6f", { "algorithm": "ed25519", "index": 0 } ] } -``` - -Response: -```json -{ "id": "2", "result": "{\"signature\":\"<hex>\",\"algorithm\":\"ed25519\",\"key_id\":\"<16 hex>\"}" } -``` - -The first positional argument is the message as hex. For `secp256k1` the `scheme` option selects `"schnorr"` (default, BIP-340) or `"ecdsa"`: - -```json -{ "id": "3", "method": "sign", "params": [ "68656c6c6f", { "algorithm": "secp256k1", "index": 0, "scheme": "ecdsa" } ] } -``` - -### 4.3 `verify` - -```json -{ "id": "4", "method": "verify", "params": [ "<message_hex>", "<signature_hex>", { "algorithm": "ed25519", "index": 0 } ] } -``` - -Response: -```json -{ "id": "4", "result": "{\"valid\":true,\"algorithm\":\"ed25519\"}" } -``` - -The signer derives its own public key from `(algorithm, index)` and verifies against it. To verify an arbitrary third-party key, use a client-side library. - -### 4.4 `encapsulate` (ML-KEM-768) - -```json -{ "id": "5", "method": "encapsulate", "params": [ "<peer_pubkey_hex>", { "algorithm": "ml-kem-768" } ] } -``` - -Response: -```json -{ "id": "5", "result": "{\"ciphertext\":\"<hex>\",\"shared_secret\":\"<hex>\",\"algorithm\":\"ml-kem-768\"}" } -``` - -`peer_pubkey_hex` is the recipient's ML-KEM-768 public key (1184 bytes → 2368 hex chars). Send the returned `ciphertext` to the recipient; both sides end up with the same `shared_secret`. - -### 4.5 `decapsulate` (ML-KEM-768) - -```json -{ "id": "6", "method": "decapsulate", "params": [ "<ciphertext_hex>", { "algorithm": "ml-kem-768", "index": 0 } ] } -``` - -Response: -```json -{ "id": "6", "result": "{\"shared_secret\":\"<hex>\",\"algorithm\":\"ml-kem-768\"}" } -``` - -### 4.6 `derive_shared_secret` (X25519) - -```json -{ "id": "7", "method": "derive_shared_secret", "params": [ "<peer_pubkey_hex>", { "algorithm": "x25519", "index": 0 } ] } -``` - -Response: -```json -{ "id": "7", "result": "{\"shared_secret\":\"<hex>\",\"algorithm\":\"x25519\"}" } -``` - -`peer_pubkey_hex` is the peer's 32-byte X25519 public key (64 hex chars). Feed the returned `shared_secret` into your own symmetric cipher (e.g. AES-GCM, ChaCha20-Poly1305). - -### 4.7 `encrypt` / `decrypt` (OTP) - -```json -{ "id": "8", "method": "encrypt", "params": [ "<plaintext_base64>", { "algorithm": "otp", "encoding": "ascii" } ] } -{ "id": "9", "method": "decrypt", "params": [ "<ciphertext>", { "algorithm": "otp", "encoding": "ascii" } ] } -``` - -`encoding` is `"ascii"` (ASCII-armored, default) or `"binary"` (base64-encoded raw `.otp` blob). If omitted on `decrypt`, auto-detection by magic bytes is used. - -`encrypt` response: -```json -{ - "id": "8", - "result": "{\"ciphertext\":\"<ascii-armor-or-base64-blob>\",\"encoding\":\"ascii\",\"pad_chksum\":\"<64 hex>\",\"pad_offset_before\":288,\"pad_offset_after\":416}" -} -``` - -`decrypt` response: -```json -{ "id": "9", "result": "{\"plaintext\":\"<base64>\",\"pad_chksum\":\"<64 hex>\"}" } -``` - -If no pad is bound at startup, the error is `-32601` `otp_pad_not_bound`. - -### 4.8 `nostr_get_public_key` - -```json -{ "id": "10", "method": "nostr_get_public_key", "params": [ { "nostr_index": 0 } ] } -``` - -Response (default): a plain 64-hex-char secp256k1 public key string. -Response with `{"format":"structured"}` in options: `{"algorithm":"secp256k1","public_key":"<hex>","key_id":"<16 hex>"}`. - -### 4.9 `nostr_sign_event` - -Serializes the event to canonical form (`[0, pubkey, created_at, kind, tags, content]`), SHA-256 hashes it to produce the event `id`, signs the hash with BIP-340 Schnorr, and returns the complete signed event. - -```json -{ "id": "11", "method": "nostr_sign_event", "params": [ "<event_json>", { "nostr_index": 0 } ] } -``` - -`<event_json>` is the unsigned event object: -```json -{ "pubkey": "...", "created_at": 1234567890, "kind": 1, "tags": [], "content": "hello" } -``` - -Response: the signed event JSON string, with `id` and `sig` populated. - -### 4.10 `nostr_mine_event` - -Mines NIP-13 proof-of-work (adds a `nonce` tag) and signs the event in one step. Mining runs in a detached thread so the signer stays responsive. - -```json -{ - "id": "12", - "method": "nostr_mine_event", - "params": [ "<event_json>", { "difficulty": 20, "threads": 4, "timeout_sec": 30, "nostr_index": 0 } ] -} -``` - -| Option | Required | Default | Meaning | -|---------------|-------------------------------|---------|---------------------------------------------------------------| -| `difficulty` | one of `difficulty`/`timeout` | 0 | Target leading zero bits. Stops early if reached. | -| `timeout_sec` | one of `difficulty`/`timeout` | 600 | Time budget in seconds. Always returns the best event found. | -| `threads` | no | 1 | Mining threads (clamped to 1..32). | - -At least one of `difficulty` or `timeout_sec` must be specified. If both are given, mining stops when **either** condition is met. Timeout is never an error — the best event found is always returned. - -Response: -```json -{ - "id": "12", - "result": "{\"event\":\"<signed event JSON with nonce tag>\",\"achieved_difficulty\":18,\"target_difficulty\":20,\"target_reached\":false,\"elapsed_sec\":30,\"attempts\":4523456}" -} -``` - -Errors: -- `1007` `no_termination_condition` — neither `difficulty` nor `timeout_sec` given. -- `1008` `mining_failed` — internal mining error. - -### 4.11 `nostr_nip04_encrypt` / `nostr_nip04_decrypt` - -NIP-04 encryption (deprecated in Nostr but still widely used): ECDH + AES-256-CBC, base64 payload. - -```json -{ "id": "13", "method": "nostr_nip04_encrypt", "params": [ "<peer_pubkey_hex>", "<plaintext>", { "nostr_index": 0 } ] } -{ "id": "14", "method": "nostr_nip04_decrypt", "params": [ "<peer_pubkey_hex>", "<ciphertext>", { "nostr_index": 0 } ] } -``` - -`encrypt` returns the NIP-04 ciphertext string; `decrypt` returns the plaintext string. - -### 4.12 `nostr_nip44_encrypt` / `nostr_nip44_decrypt` - -NIP-44 encryption (current Nostr standard): ECDH + HKDF + ChaCha20-Poly1305 + specific payload format. - -```json -{ "id": "15", "method": "nostr_nip44_encrypt", "params": [ "<peer_pubkey_hex>", "<plaintext>", { "nostr_index": 0 } ] } -{ "id": "16", "method": "nostr_nip44_decrypt", "params": [ "<peer_pubkey_hex>", "<ciphertext>", { "nostr_index": 0 } ] } -``` - -`encrypt` returns the NIP-44 ciphertext string; `decrypt` returns the plaintext string. - ---- - -## 5. Transports - -The API is transport-independent. The same JSON request works over every transport; only the framing differs. - -| Transport | `--listen` flag | Framing | Caller identity | -|-----------|--------------------------------|------------------------------------------|----------------------------| -| Unix socket (abstract) | `unix` (default on desktop) | Length-prefixed framed JSON | `SO_PEERCRED` → `uid:<n>` | -| stdio | `stdio` | One framed request/response over stdin/stdout | inherited uid | -| qrexec | `qrexec` | Same as stdio; caller from `QREXEC_REMOTE_DOMAIN` | `qubes:<vm>` | -| FIPS/TCP | `tcp:[host]:port` | Length-prefixed framed JSON | (transport-defined) | -| HTTP | `http:host:port` | Standard HTTP POST, JSON body, no custom framing. CORS enabled. | (transport-defined) | - -### 5.1 HTTP examples - -Start the signer: -```bash -nsigner --listen http:127.0.0.1:11111 --allow-all -``` - -Get a public key: -```bash -curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \ - -d '{"id":"1","method":"get_public_key","params":[{"algorithm":"ed25519","index":0}]}' -``` - -Sign a Nostr event: -```bash -curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \ - -d '{"id":"1","method":"nostr_sign_event","params":[{"pubkey":"...","created_at":1234567890,"kind":1,"tags":[],"content":"hello"},{"nostr_index":0}]}' -``` - -OTP encrypt: -```bash -curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \ - -d '{"id":"1","method":"encrypt","params":["SGVsbG8sIE9UUCB3b3JsZCE=",{"algorithm":"otp","encoding":"ascii"}]}' -``` - -### 5.2 Unix socket examples (framed mode) - -```bash -# get_public_key -nsigner --socket-name nsigner client \ - '{"id":"1","method":"get_public_key","params":[{"algorithm":"ed25519","index":0}]}' - -# Sign a Nostr event -nsigner --socket-name nsigner client \ - '{"id":"2","method":"nostr_sign_event","params":[{"pubkey":"...","created_at":1234567890,"kind":1,"tags":[],"content":"hello"},{"nostr_index":0}]}' - -# ed25519 sign -nsigner --socket-name nsigner client \ - '{"id":"3","method":"sign","params":["68656c6c6f",{"algorithm":"ed25519","index":0}]}' -``` - ---- - -## 6. Pre-approval - -Pre-approval entries skip the interactive prompt for matching requests. They are configured at startup with `--preapprove`. - -### 6.1 Algorithm-based - -```bash -nsigner --preapprove caller=uid:1000,algorithm=ed25519,index=0-4,verb=sign,verify -nsigner --preapprove caller=uid:1000,algorithm=ml-kem-768,index=0,verb=decapsulate -``` - -### 6.2 Nostr - -```bash -nsigner --preapprove caller=uid:1000,nostr_index=0,verb=nostr_sign_event,nostr_get_public_key -``` - -A `*` wildcard matches any caller, role, or verb. Index ranges use `min-max` syntax. Unmatched requests fall through to the default policy (prompt for same-uid, deny for others). +For the security model, transports, and operational behavior, see [`README.md`](README.md) §1–§3 and §5–§8. For the migration plan from the legacy verb names, see [`plans/legacy_verb_aliases.md`](plans/legacy_verb_aliases.md). diff --git a/client/README.md b/client/README.md index 49e967b..4eaac64 100644 --- a/client/README.md +++ b/client/README.md @@ -22,8 +22,8 @@ for the full integration contract. |---|---| | `nsigner_client_t` (stack) | `nsigner_client_t*` (heap) or `nostr_signer_t*` | | `nsigner_client_init` / `connect_unix` / `close` | `nsigner_transport_open_unix` + `nsigner_client_new` / `nsigner_client_free` | -| `nsigner_client_get_public_key` | `nostr_signer_get_public_key` or `nsigner_client_call(..., "get_public_key", ...)` | -| `nsigner_client_sign_event` | `nostr_signer_sign_event` or `nsigner_client_call(..., "sign_event", ...)` | +| `nsigner_client_get_public_key` | `nostr_signer_get_public_key` or `nsigner_client_call(..., "nostr_get_public_key", ...)` | +| `nsigner_client_sign_event` | `nostr_signer_sign_event` or `nsigner_client_call(..., "nostr_sign_event", ...)` | | `nsigner_client_set_auth` | `nsigner_client_set_auth` or `nostr_signer_nsigner_set_auth` | | `nsigner_client_request` / `request_raw` | `nsigner_client_call` (returns parsed cJSON result) | @@ -42,51 +42,46 @@ n_signer supports six algorithms: `secp256k1` (Nostr), `ed25519` (SSH), `x25519` (age/ECDH), `ml-dsa-65` (PQ signatures, FIPS 204), `slh-dsa-128s` (PQ hash-based signatures, FIPS 205), and `ml-kem-768` (PQ KEM, FIPS 203). -Use `nsigner_client_call(client, "<verb>", params, &result)` with these verbs: - -| Verb | Algorithms | Description | -|---|---|---| -| `get_public_key` | all | Returns the role's public key (see format below) | -| `sign_event` | secp256k1 | Sign a Nostr event (existing) | -| `nip44_encrypt` / `nip44_decrypt` | secp256k1 | NIP-44 (existing) | -| `nip04_encrypt` / `nip04_decrypt` | secp256k1 | NIP-04 (existing) | -| `mine_event` | secp256k1 | NIP-13 PoW mining + sign (existing) | -| `sign_data` | ed25519, ml-dsa-65, slh-dsa-128s | Sign arbitrary bytes (hex) | -| `verify_signature` | ed25519, ml-dsa-65, slh-dsa-128s | Verify a signature against the role's pubkey | -| `ssh_sign` | ed25519 | Sign an SSH authentication challenge | -| `kem_encapsulate` | ml-kem-768 | Encapsulate with a peer's ML-KEM public key | -| `kem_decapsulate` | ml-kem-768 | Decapsulate a ciphertext with the role's ML-KEM private key | - -### Algorithm-based API (new) - -In addition to the role-based verbs above, the signer supports algorithm-based verbs where the caller specifies `algorithm` and `index` directly, without needing a role: +The API has two verb families (see [`README.md`](../README.md#4-api) §4 for the full spec): + +**Algorithm-based verbs** — the caller specifies `algorithm` and `index` in the +options object. No role table entry is needed. | Verb | Algorithms | Description | |---|---|---| +| `get_public_key` | all key-deriving algorithms | Returns the derived public key (structured) | | `sign` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | Sign arbitrary bytes (hex) | | `verify` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | Verify a signature | | `encapsulate` | ml-kem-768 | KEM encapsulation with peer's public key | | `decapsulate` | ml-kem-768 | KEM decapsulation with derived private key | | `derive_shared_secret` | x25519 | ECDH key agreement | -| `get_public_key` (with `algorithm`) | all | Get public key for a derived key | +| `encrypt` / `decrypt` | otp | One-time pad encrypt/decrypt (`algorithm:"otp"`) | + +**Nostr protocol verbs** — select a secp256k1 NIP-06 key via `nostr_index` (or +`role`/`role_path`). These are role-based. + +| Verb | Description | +|---|---| +| `nostr_get_public_key` | Returns the role's secp256k1 public key | +| `nostr_sign_event` | Sign a Nostr event | +| `nostr_mine_event` | NIP-13 PoW mining + sign | +| `nostr_nip44_encrypt` / `nostr_nip44_decrypt` | NIP-44 encrypt/decrypt | +| `nostr_nip04_encrypt` / `nostr_nip04_decrypt` | NIP-04 encrypt/decrypt | Example: `nsigner_client_call(client, "sign", "[\"68656c6c6f\",{\"algorithm\":\"ed25519\",\"index\":0}]", &result)` For secp256k1, the optional `scheme` parameter selects `"schnorr"` (default) or `"ecdsa"`. -Old verbs (`sign_data`, `ssh_sign`, `verify_signature`, `kem_encapsulate`, `kem_decapsulate`) also accept the `algorithm` parameter and map to the new verbs. Without `algorithm`, they use the role-based path (backward compatible). - ### `get_public_key` response format -- **secp256k1 (backward compatible):** `result` is a plain hex string - (`cJSON_IsString(result)` is true, 64 hex chars). -- **secp256k1 with `{"format":"structured"}` option:** `result` is a JSON - string containing `{"algorithm":"secp256k1","public_key":"<hex>","key_id":"<16 hex>"}`. -- **All other algorithms:** `result` is always a JSON string containing - `{"algorithm":"<alg>","public_key":"<hex>","key_id":"<16 hex>"}`. +The algorithm-based `get_public_key` always returns a structured JSON string: +`{"algorithm":"<alg>","public_key":"<hex>","key_id":"<16 hex>"}`. + +The role-based `nostr_get_public_key` returns a plain 64-hex-char secp256k1 +public key by default, or the structured form with `{"format":"structured"}`. Clients should parse the `result` string with `cJSON_Parse` to extract the -`algorithm`, `public_key`, and `key_id` fields for non-secp256k1 algorithms. +`algorithm`, `public_key`, and `key_id` fields when the result is a JSON object. ### Key sizes diff --git a/client/demo_c99.c b/client/demo_c99.c index dfc96e6..7c4fbef 100644 --- a/client/demo_c99.c +++ b/client/demo_c99.c @@ -3,12 +3,12 @@ * via Qubes qrexec and performing all three core operations: * * 1. get_public_key — retrieve a Nostr public key by nostr_index - * 2. sign_event — sign a Nostr event (kind 1 text note) - * 3. nip44_encrypt — encrypt a message to a peer (and decrypt it back) + * 2. nostr_sign_event — sign a Nostr event (kind 1 text note) + * 3. nostr_nip44_encrypt — encrypt a message to a peer (and decrypt it back) * - * Note: mine_event (NIP-13 PoW) is also available via the JSON-RPC interface. - * See demo_javascript.js and demo_python.py for mine_event usage examples. - * The high-level nostr_signer API does not yet wrap mine_event. + * Note: nostr_mine_event (NIP-13 PoW) is also available via the JSON-RPC interface. + * See demo_javascript.js and demo_python.py for nostr_mine_event usage examples. + * The high-level nostr_signer API does not yet wrap nostr_mine_event. * * This uses the high-level nostr_signer API from nostr_core_lib: * - nostr_signer_nsigner_qrexec() — qrexec transport (no network) @@ -132,7 +132,7 @@ static int demo_sign_event(nostr_signer_t *signer, const char *pubkey_hex) { char *signed_json = NULL; int rc; - printf("\n=== Demo 2: sign_event (kind 1 text note) ===\n"); + printf("\n=== Demo 2: nostr_sign_event (kind 1 text note) ===\n"); /* Build an unsigned Nostr event (kind 1 text note) */ unsigned_event = cJSON_CreateObject(); @@ -163,7 +163,7 @@ static int demo_sign_event(nostr_signer_t *signer, const char *pubkey_hex) { /* Sign it */ rc = nostr_signer_sign_event(signer, unsigned_event, &signed_event); if (rc != NOSTR_SUCCESS) { - print_error("sign_event", rc); + print_error("nostr_nostr_sign_event", rc); cJSON_Delete(unsigned_event); return rc; } @@ -204,14 +204,14 @@ static int demo_nip44(nostr_signer_t *signer, const char *pubkey_hex) { char *decrypted = NULL; int rc; - printf("\n=== Demo 3: nip44_encrypt / nip44_decrypt ===\n"); + printf("\n=== Demo 3: nostr_nip44_encrypt / nostr_nip44_decrypt ===\n"); printf(" plaintext: \"%s\"\n", plaintext); printf(" peer pubkey: %s (self)\n", pubkey_hex); /* Encrypt */ rc = nostr_signer_nip44_encrypt(signer, pubkey_hex, plaintext, &ciphertext); if (rc != NOSTR_SUCCESS) { - print_error("nip44_encrypt", rc); + print_error("nostr_nostr_nip44_encrypt", rc); return rc; } @@ -220,7 +220,7 @@ static int demo_nip44(nostr_signer_t *signer, const char *pubkey_hex) { /* Decrypt (using our own pubkey as the sender) */ rc = nostr_signer_nip44_decrypt(signer, pubkey_hex, ciphertext, &decrypted); if (rc != NOSTR_SUCCESS) { - print_error("nip44_decrypt", rc); + print_error("nostr_nostr_nip44_decrypt", rc); free(ciphertext); return rc; } diff --git a/client/demo_javascript.js b/client/demo_javascript.js index 08f1799..2b6ca38 100644 --- a/client/demo_javascript.js +++ b/client/demo_javascript.js @@ -4,8 +4,8 @@ * running n_signer via Qubes qrexec and performing all three core operations: * * 1. get_public_key — retrieve a Nostr public key by nostr_index - * 2. sign_event — sign a Nostr event (kind 1 text note) - * 3. nip44_encrypt — encrypt a message to a peer (and decrypt it back) + * 2. nostr_sign_event — sign a Nostr event (kind 1 text note) + * 3. nostr_nip44_encrypt — encrypt a message to a peer (and decrypt it back) * * Uses qrexec-client-vm (Qubes OS inter-qube IPC). No auth envelope needed — * identity comes from QREXEC_REMOTE_DOMAIN on the server side. @@ -120,7 +120,7 @@ async function demoGetPublicKey(targetQube, nostrIndex) { * Demo 2: Sign a Nostr event (kind 1 text note). */ async function demoSignEvent(targetQube, nostrIndex, pubkeyHex) { - console.log("\n=== Demo 2: sign_event (kind 1 text note) ==="); + console.log("\n=== Demo 2: nostr_sign_event (kind 1 text note) ==="); const unsignedEvent = { kind: 1, @@ -135,12 +135,12 @@ async function demoSignEvent(targetQube, nostrIndex, pubkeyHex) { const response = await callNsigner(targetQube, { id: "2", - method: "sign_event", + method: "nostr_nostr_sign_event", params: [JSON.stringify(unsignedEvent), { nostr_index: nostrIndex }], }); if (response.error) { - throw new Error(`sign_event failed: ${JSON.stringify(response.error)}`); + throw new Error(`nostr_sign_event failed: ${JSON.stringify(response.error)}`); } const signedEvent = JSON.parse(response.result); @@ -159,19 +159,19 @@ async function demoSignEvent(targetQube, nostrIndex, pubkeyHex) { async function demoNip44(targetQube, nostrIndex, pubkeyHex) { const plaintext = "Secret message from n_signer JavaScript demo!"; - console.log("\n=== Demo 3: nip44_encrypt / nip44_decrypt ==="); + console.log("\n=== Demo 3: nostr_nip44_encrypt / nostr_nip44_decrypt ==="); console.log(` plaintext: "${plaintext}"`); console.log(` peer pubkey: ${pubkeyHex} (self)`); // Encrypt const encResponse = await callNsigner(targetQube, { id: "3", - method: "nip44_encrypt", + method: "nostr_nostr_nip44_encrypt", params: [pubkeyHex, plaintext, { nostr_index: nostrIndex }], }); if (encResponse.error) { - throw new Error(`nip44_encrypt failed: ${JSON.stringify(encResponse.error)}`); + throw new Error(`nostr_nip44_encrypt failed: ${JSON.stringify(encResponse.error)}`); } const ciphertext = encResponse.result; @@ -180,12 +180,12 @@ async function demoNip44(targetQube, nostrIndex, pubkeyHex) { // Decrypt const decResponse = await callNsigner(targetQube, { id: "4", - method: "nip44_decrypt", + method: "nostr_nostr_nip44_decrypt", params: [pubkeyHex, ciphertext, { nostr_index: nostrIndex }], }); if (decResponse.error) { - throw new Error(`nip44_decrypt failed: ${JSON.stringify(decResponse.error)}`); + throw new Error(`nostr_nip44_decrypt failed: ${JSON.stringify(decResponse.error)}`); } const decrypted = decResponse.result; @@ -199,7 +199,7 @@ async function demoNip44(targetQube, nostrIndex, pubkeyHex) { } async function demoMineEvent(targetQube, nostrIndex) { - console.log("\n--- Demo 4: mine_event (NIP-13 Proof-of-Work) ---"); + console.log("\n--- Demo 4: nostr_mine_event (NIP-13 Proof-of-Work) ---"); const event = { kind: 1, @@ -210,7 +210,7 @@ async function demoMineEvent(targetQube, nostrIndex) { console.log(" Mining with difficulty=4, threads=4, timeout_sec=30..."); const response = await callNsigner(targetQube, { id: "5", - method: "mine_event", + method: "nostr_nostr_mine_event", params: [JSON.stringify(event), { difficulty: 4, threads: 4, @@ -220,7 +220,7 @@ async function demoMineEvent(targetQube, nostrIndex) { }); if (response.error) { - throw new Error(`mine_event failed: ${JSON.stringify(response.error)}`); + throw new Error(`nostr_mine_event failed: ${JSON.stringify(response.error)}`); } const result = JSON.parse(response.result); diff --git a/client/demo_python.py b/client/demo_python.py index d7f638c..bac3398 100644 --- a/client/demo_python.py +++ b/client/demo_python.py @@ -4,8 +4,8 @@ demo_python.py — comprehensive Python demo for connecting to a running n_signe via Qubes qrexec and performing all three core operations: 1. get_public_key — retrieve a Nostr public key by nostr_index - 2. sign_event — sign a Nostr event (kind 1 text note) - 3. nip44_encrypt — encrypt a message to a peer (and decrypt it back) + 2. nostr_sign_event — sign a Nostr event (kind 1 text note) + 3. nostr_nip44_encrypt — encrypt a message to a peer (and decrypt it back) Uses qrexec-client-vm (Qubes OS inter-qube IPC). No auth envelope needed — identity comes from QREXEC_REMOTE_DOMAIN on the server side. @@ -148,7 +148,7 @@ def demo_get_public_key(target_qube, nostr_index): def demo_sign_event(target_qube, nostr_index, pubkey_hex): """Demo 2: Sign a Nostr event (kind 1 text note).""" - print("\n=== Demo 2: sign_event (kind 1 text note) ===") + print("\n=== Demo 2: nostr_sign_event (kind 1 text note) ===") unsigned_event = { "kind": 1, @@ -165,13 +165,13 @@ def demo_sign_event(target_qube, nostr_index, pubkey_hex): target_qube, { "id": "2", - "method": "sign_event", + "method": "nostr_nostr_sign_event", "params": [json.dumps(unsigned_event, separators=(",", ":")), {"nostr_index": nostr_index}], }, ) if "error" in response: - raise RuntimeError(f"sign_event failed: {json.dumps(response['error'])}") + raise RuntimeError(f"nostr_sign_event failed: {json.dumps(response['error'])}") signed_event = json.loads(response["result"]) print(" Signed event:") @@ -185,7 +185,7 @@ def demo_nip44(target_qube, nostr_index, pubkey_hex): """Demo 3: NIP-44 encrypt and decrypt.""" plaintext = "Secret message from n_signer Python demo!" - print("\n=== Demo 3: nip44_encrypt / nip44_decrypt ===") + print("\n=== Demo 3: nostr_nip44_encrypt / nostr_nip44_decrypt ===") print(f' plaintext: "{plaintext}"') print(f" peer pubkey: {pubkey_hex} (self)") @@ -194,13 +194,13 @@ def demo_nip44(target_qube, nostr_index, pubkey_hex): target_qube, { "id": "3", - "method": "nip44_encrypt", + "method": "nostr_nostr_nip44_encrypt", "params": [pubkey_hex, plaintext, {"nostr_index": nostr_index}], }, ) if "error" in enc_response: - raise RuntimeError(f"nip44_encrypt failed: {json.dumps(enc_response['error'])}") + raise RuntimeError(f"nostr_nip44_encrypt failed: {json.dumps(enc_response['error'])}") ciphertext = enc_response["result"] print(f" ciphertext: {ciphertext}") @@ -210,13 +210,13 @@ def demo_nip44(target_qube, nostr_index, pubkey_hex): target_qube, { "id": "4", - "method": "nip44_decrypt", + "method": "nostr_nostr_nip44_decrypt", "params": [pubkey_hex, ciphertext, {"nostr_index": nostr_index}], }, ) if "error" in dec_response: - raise RuntimeError(f"nip44_decrypt failed: {json.dumps(dec_response['error'])}") + raise RuntimeError(f"nostr_nip44_decrypt failed: {json.dumps(dec_response['error'])}") decrypted = dec_response["result"] print(f' decrypted: "{decrypted}"') @@ -227,8 +227,8 @@ def demo_nip44(target_qube, nostr_index, pubkey_hex): raise RuntimeError("Round-trip FAILED: plaintext does not match decrypted") -def demo_mine_event(target_qube, nostr_index): - print("\n--- Demo 4: mine_event (NIP-13 Proof-of-Work) ---") +def demo_nostr_mine_event(target_qube, nostr_index): + print("\n--- Demo 4: nostr_mine_event (NIP-13 Proof-of-Work) ---") event = {"kind": 1, "content": "Hello Nostr with PoW!", "tags": []} @@ -237,7 +237,7 @@ def demo_mine_event(target_qube, nostr_index): target_qube, { "id": "5", - "method": "mine_event", + "method": "nostr_nostr_mine_event", "params": [json.dumps(event), { "difficulty": 4, "threads": 4, @@ -248,7 +248,7 @@ def demo_mine_event(target_qube, nostr_index): ) if "error" in response: - raise RuntimeError(f"mine_event failed: {json.dumps(response['error'])}") + raise RuntimeError(f"nostr_mine_event failed: {json.dumps(response['error'])}") result = json.loads(response["result"]) print(f" achieved_difficulty: {result['achieved_difficulty']}") @@ -282,7 +282,7 @@ def main(): pubkey_hex = demo_get_public_key(target_qube, nostr_index) demo_sign_event(target_qube, nostr_index, pubkey_hex) demo_nip44(target_qube, nostr_index, pubkey_hex) - demo_mine_event(target_qube, nostr_index) + demo_nostr_mine_event(target_qube, nostr_index) print("\n=== Summary ===") print("All demos completed successfully.") diff --git a/examples/feather_sign_event.py b/examples/feather_sign_event.py index 2ce8980..4992db9 100644 --- a/examples/feather_sign_event.py +++ b/examples/feather_sign_event.py @@ -140,11 +140,11 @@ def main() -> int: req = { "jsonrpc": "2.0", "id": "2", - "method": "sign_event", + "method": "nostr_sign_event", "params": params, } if not no_auth: - req["auth"] = build_auth_envelope("sign_event", params, caller_priv) + req["auth"] = build_auth_envelope("nostr_sign_event", params, caller_priv) body = json.dumps(req, separators=(",", ":")).encode("utf-8") frame = struct.pack(">I", len(body)) + body diff --git a/examples/feather_webusb_demo.html b/examples/feather_webusb_demo.html index 51208e9..a6fe644 100644 --- a/examples/feather_webusb_demo.html +++ b/examples/feather_webusb_demo.html @@ -460,7 +460,7 @@ }; const params = [unsignedEvent, getIndexOptions()]; - const resp = await rpcCall("sign_event", params, "web-sign-kind1"); + const resp = await rpcCall("nostr_sign_event", params, "web-sign-kind1"); signOutEl.textContent = pretty(resp?.result ?? resp); } catch (e) { signOutEl.textContent = String(e); @@ -472,7 +472,7 @@ const peer = requirePeerHex(nip04PeerEl.value); const msg = String(nip04MsgEl.value || ""); const params = [peer, msg, getIndexOptions()]; - const resp = await rpcCall("nip04_encrypt", params, "web-nip04-enc"); + const resp = await rpcCall("nostr_nip04_encrypt", params, "web-nip04-enc"); nip04OutEl.textContent = pretty(resp?.result ?? resp); if (resp && typeof resp.result === "string") { nip04DecPeerEl.value = peer; @@ -488,7 +488,7 @@ const peer = requirePeerHex(nip04DecPeerEl.value); const ciphertext = String(nip04CipherEl.value || ""); const params = [peer, ciphertext, getIndexOptions()]; - const resp = await rpcCall("nip04_decrypt", params, "web-nip04-dec"); + const resp = await rpcCall("nostr_nip04_decrypt", params, "web-nip04-dec"); nip04DecOutEl.textContent = requireStringResult(resp, "NIP-04 decrypt"); } catch (e) { nip04DecOutEl.textContent = String(e); @@ -500,7 +500,7 @@ const peer = requirePeerHex(nip44PeerEl.value); const msg = String(nip44MsgEl.value || ""); const params = [peer, msg, getIndexOptions()]; - const resp = await rpcCall("nip44_encrypt", params, "web-nip44-enc"); + const resp = await rpcCall("nostr_nip44_encrypt", params, "web-nip44-enc"); nip44OutEl.textContent = pretty(resp?.result ?? resp); if (resp && typeof resp.result === "string") { nip44DecPeerEl.value = peer; @@ -516,7 +516,7 @@ const peer = requirePeerHex(nip44DecPeerEl.value); const ciphertext = String(nip44CipherEl.value || ""); const params = [peer, ciphertext, getIndexOptions()]; - const resp = await rpcCall("nip44_decrypt", params, "web-nip44-dec"); + const resp = await rpcCall("nostr_nip44_decrypt", params, "web-nip44-dec"); nip44DecOutEl.textContent = requireStringResult(resp, "NIP-44 decrypt"); } catch (e) { nip44DecOutEl.textContent = String(e); diff --git a/examples/get_pubkey_tcp.c b/examples/get_pubkey_tcp.c index c439d5b..8d10436 100644 --- a/examples/get_pubkey_tcp.c +++ b/examples/get_pubkey_tcp.c @@ -95,7 +95,7 @@ static int query_pubkey(const char *host, int port, int nostr_index, cJSON_AddItemToArray(params, opts); opts = NULL; - if (nsigner_client_call(client, "get_public_key", params, &result) != NOSTR_SUCCESS) { + if (nsigner_client_call(client, "nostr_get_public_key", params, &result) != NOSTR_SUCCESS) { fprintf(stderr, "request failed for index %d: %s\n", nostr_index, nsigner_client_last_error(client)); params = NULL; /* nsigner_client_call took ownership even on failure */ diff --git a/examples/get_public_key_client.c b/examples/get_public_key_client.c index 69b0120..9ca45e2 100644 --- a/examples/get_public_key_client.c +++ b/examples/get_public_key_client.c @@ -59,7 +59,7 @@ int main(int argc, char **argv) { goto cleanup; } - if (nsigner_client_call(client, "get_public_key", params, &result) != NOSTR_SUCCESS) { + if (nsigner_client_call(client, "nostr_get_public_key", params, &result) != NOSTR_SUCCESS) { fprintf(stderr, "request failed: %s\n", nsigner_client_last_error(client)); goto cleanup; } diff --git a/examples/kb2040_hidden_signer_client.py b/examples/kb2040_hidden_signer_client.py index e3dc5d0..fe3d0dc 100644 --- a/examples/kb2040_hidden_signer_client.py +++ b/examples/kb2040_hidden_signer_client.py @@ -167,7 +167,7 @@ def cmd_get_public_key(args): def cmd_sign_event(args): event = json.loads(args.event) - print(json.dumps(rpc(args, "sign_event", {"event": event}), indent=2)) + print(json.dumps(rpc(args, "nostr_sign_event", {"event": event}), indent=2)) def build_parser(): diff --git a/examples/otp_nostr_30078.py b/examples/otp_nostr_30078.py index fb8cf4a..a8fc421 100644 --- a/examples/otp_nostr_30078.py +++ b/examples/otp_nostr_30078.py @@ -141,8 +141,8 @@ def main(): pt_b64 = base64.b64encode(plaintext.encode()).decode() resp = run_one_request({ "id": "2", - "method": "otp_encrypt", - "params": [pt_b64, {"encoding": "ascii"}], + "method": "encrypt", + "params": [pt_b64, {"algorithm": "otp", "encoding": "ascii"}], }) if resp is None or "result" not in resp: print("ERROR: otp_encrypt failed") @@ -191,7 +191,7 @@ def main(): } resp = run_one_request({ "id": "3", - "method": "sign_event", + "method": "nostr_sign_event", "params": [json.dumps(event_for_signing), {"role": "main"}], }) if resp is None or "result" not in resp: diff --git a/examples/pq_kem_example.c b/examples/pq_kem_example.c index 69ee097..a32bdbe 100644 --- a/examples/pq_kem_example.c +++ b/examples/pq_kem_example.c @@ -49,7 +49,8 @@ static int get_structured_pubkey(nsigner_client_t *client, const char *role, cJSON_Delete(params); return -1; } - cJSON_AddStringToObject(opts, "role", role); + cJSON_AddStringToObject(opts, "algorithm", "ml-kem-768"); + cJSON_AddNumberToObject(opts, "index", 0); cJSON_AddItemToArray(params, opts); opts = NULL; @@ -99,11 +100,12 @@ static int kem_encapsulate(nsigner_client_t *client, const char *role, cJSON_Delete(params); return -1; } - cJSON_AddStringToObject(opts, "role", role); + cJSON_AddStringToObject(opts, "algorithm", "ml-kem-768"); + cJSON_AddNumberToObject(opts, "index", 0); cJSON_AddItemToArray(params, opts); opts = NULL; - if (nsigner_client_call(client, "kem_encapsulate", params, &result) != NOSTR_SUCCESS) { + if (nsigner_client_call(client, "encapsulate", params, &result) != NOSTR_SUCCESS) { cJSON_Delete(params); return -1; } @@ -147,11 +149,12 @@ static char *kem_decapsulate(nsigner_client_t *client, const char *role, cJSON_Delete(params); return NULL; } - cJSON_AddStringToObject(opts, "role", role); + cJSON_AddStringToObject(opts, "algorithm", "ml-kem-768"); + cJSON_AddNumberToObject(opts, "index", 0); cJSON_AddItemToArray(params, opts); opts = NULL; - if (nsigner_client_call(client, "kem_decapsulate", params, &result) != NOSTR_SUCCESS) { + if (nsigner_client_call(client, "decapsulate", params, &result) != NOSTR_SUCCESS) { cJSON_Delete(params); return NULL; } diff --git a/examples/pq_sign_example.c b/examples/pq_sign_example.c index c376508..47b98d0 100644 --- a/examples/pq_sign_example.c +++ b/examples/pq_sign_example.c @@ -46,7 +46,8 @@ static int get_structured_pubkey(nsigner_client_t *client, const char *role, cJSON_Delete(params); return -1; } - cJSON_AddStringToObject(opts, "role", role); + cJSON_AddStringToObject(opts, "algorithm", "ml-dsa-65"); + cJSON_AddNumberToObject(opts, "index", 0); cJSON_AddItemToArray(params, opts); opts = NULL; @@ -89,11 +90,12 @@ static char *sign_data(nsigner_client_t *client, const char *role, cJSON_Delete(params); return NULL; } - cJSON_AddStringToObject(opts, "role", role); + cJSON_AddStringToObject(opts, "algorithm", "ml-dsa-65"); + cJSON_AddNumberToObject(opts, "index", 0); cJSON_AddItemToArray(params, opts); opts = NULL; - if (nsigner_client_call(client, "sign_data", params, &result) != NOSTR_SUCCESS) { + if (nsigner_client_call(client, "sign", params, &result) != NOSTR_SUCCESS) { cJSON_Delete(params); return NULL; } diff --git a/examples/sign_event_client.c b/examples/sign_event_client.c index 51764f8..38a3b8d 100644 --- a/examples/sign_event_client.c +++ b/examples/sign_event_client.c @@ -73,7 +73,7 @@ int main(int argc, char **argv) { cJSON_AddItemToArray(params, opts); opts = NULL; /* owned by params now */ - if (nsigner_client_call(client, "sign_event", params, &result) != NOSTR_SUCCESS) { + if (nsigner_client_call(client, "nostr_sign_event", params, &result) != NOSTR_SUCCESS) { fprintf(stderr, "request failed: %s\n", nsigner_client_last_error(client)); goto cleanup; } diff --git a/examples/ssh_sign_example.c b/examples/ssh_sign_example.c index c4f3d97..d9bc1a5 100644 --- a/examples/ssh_sign_example.c +++ b/examples/ssh_sign_example.c @@ -47,7 +47,8 @@ static int get_structured_pubkey(nsigner_client_t *client, const char *role, cJSON_Delete(params); return -1; } - cJSON_AddStringToObject(opts, "role", role); + cJSON_AddStringToObject(opts, "algorithm", "ed25519"); + cJSON_AddNumberToObject(opts, "index", 0); cJSON_AddItemToArray(params, opts); opts = NULL; @@ -97,11 +98,12 @@ static char *ssh_sign(nsigner_client_t *client, const char *role, cJSON_Delete(params); return NULL; } - cJSON_AddStringToObject(opts, "role", role); + cJSON_AddStringToObject(opts, "algorithm", "ed25519"); + cJSON_AddNumberToObject(opts, "index", 0); cJSON_AddItemToArray(params, opts); opts = NULL; - if (nsigner_client_call(client, "ssh_sign", params, &result) != NOSTR_SUCCESS) { + if (nsigner_client_call(client, "sign", params, &result) != NOSTR_SUCCESS) { cJSON_Delete(params); return NULL; } diff --git a/plans/legacy_verb_aliases.md b/plans/legacy_verb_aliases.md index a3ed08f..5194304 100644 --- a/plans/legacy_verb_aliases.md +++ b/plans/legacy_verb_aliases.md @@ -1,4 +1,6 @@ -# Plan: Legacy Verb Aliases (Migration Path) +# Plan: Legacy Verb Aliases (Migration Path) — COMPLETED + +> **Status: Done.** All steps below have been executed. The legacy verb names are gone from the wire protocol, the implementation, the tests, the clients, and the documentation. `make dev` builds clean and `make test` passes (all 10 test binaries, 230+ assertions). ## Context diff --git a/src/dispatcher.c b/src/dispatcher.c index 2d1c76a..fb29124 100644 --- a/src/dispatcher.c +++ b/src/dispatcher.c @@ -204,38 +204,25 @@ const char *selector_strerror(int err); #define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */ #define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */ -/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */ +/* Algorithm-based verb defines (algorithm is a parameter, not implicit). + * Callers must supply `algorithm` explicitly — no implicit defaults. */ #define VERB_SIGN "sign" #define VERB_VERIFY "verify" #define VERB_ENCAPSULATE "encapsulate" #define VERB_DECAPSULATE "decapsulate" #define VERB_DERIVE_SHARED "derive_shared_secret" +#define VERB_GET_PUBLIC_KEY "get_public_key" -#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */ +/* Nostr protocol verbs (secp256k1 NIP-06, role-based selector). */ +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_MINE_EVENT "nostr_mine_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" -/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */ -#define VERB_SIGN "sign" -#define VERB_VERIFY "verify" -#define VERB_ENCAPSULATE "encapsulate" -#define VERB_DECAPSULATE "decapsulate" -#define VERB_DERIVE_SHARED "derive_shared_secret" - - -/* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" -#define VERB_MINE_EVENT "mine_event" -#define VERB_SIGN_DATA "sign_data" -#define VERB_VERIFY_SIG "verify_signature" -#define VERB_SSH_SIGN "ssh_sign" -#define VERB_KEM_ENCAPS "kem_encapsulate" -#define VERB_KEM_DECAPS "kem_decapsulate" -#define VERB_OTP_ENCRYPT "otp_encrypt" -#define VERB_OTP_DECRYPT "otp_decrypt" +/* OTP verbs (one-time pad; selected via algorithm:"otp"). */ #define VERB_ENCRYPT "encrypt" #define VERB_DECRYPT "decrypt" @@ -244,7 +231,7 @@ const char *selector_strerror(int err); * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. * * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: + * - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require: * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). */ @@ -825,7 +812,8 @@ void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_ * to the role-based path). */ -/* Check whether a method name is an algorithm-based verb (new or alias). */ +/* Check whether a method name is an algorithm-based verb. + * These verbs take an explicit `algorithm` option and bypass the role table. */ static int is_algorithm_verb(const char *method) { if (method == NULL) return 0; return (strcmp(method, VERB_SIGN) == 0 || @@ -833,24 +821,7 @@ static int is_algorithm_verb(const char *method) { strcmp(method, VERB_ENCAPSULATE) == 0 || strcmp(method, VERB_DECAPSULATE) == 0 || strcmp(method, VERB_DERIVE_SHARED) == 0 || - /* aliases */ - strcmp(method, VERB_SIGN_DATA) == 0 || - strcmp(method, VERB_SSH_SIGN) == 0 || - strcmp(method, VERB_VERIFY_SIG) == 0 || - strcmp(method, VERB_KEM_ENCAPS) == 0 || - strcmp(method, VERB_KEM_DECAPS) == 0); -} - -/* Map an alias verb to its canonical algorithm-based verb name. - * Returns the canonical verb, or the original if not an alias. */ -static const char *canonical_alg_verb(const char *method) { - if (method == NULL) return method; - if (strcmp(method, VERB_SIGN_DATA) == 0) return VERB_SIGN; - if (strcmp(method, VERB_SSH_SIGN) == 0) return VERB_SIGN; - if (strcmp(method, VERB_VERIFY_SIG) == 0) return VERB_VERIFY; - if (strcmp(method, VERB_KEM_ENCAPS) == 0) return VERB_ENCAPSULATE; - if (strcmp(method, VERB_KEM_DECAPS) == 0) return VERB_DECAPSULATE; - return method; + strcmp(method, VERB_GET_PUBLIC_KEY) == 0); } /* Parse the algorithm string from the options object. @@ -909,7 +880,6 @@ static char *build_alg_result_json(const char *alg_name, const char *key_id, static char *handle_algorithm_verb(dispatcher_ctx_t *ctx, const char *id_str, const char *method, cJSON *params_item, cJSON *options_item) { - const char *canonical; crypto_alg_t alg; int index; int enforce_rc; @@ -917,25 +887,14 @@ static char *handle_algorithm_verb(dispatcher_ctx_t *ctx, const char *id_str, const alg_key_entry_t *key_entry; const crypto_alg_sizes_t *sz; - if (!is_algorithm_verb(method) && strcmp(method, VERB_GET_PUBLIC_KEY) != 0) { + if (!is_algorithm_verb(method)) { return NULL; /* not an algorithm-based verb */ } - canonical = canonical_alg_verb(method); - - /* Parse algorithm and index from options. */ + /* Parse algorithm and index from options. Callers must supply + * `algorithm` explicitly — no implicit defaults. */ alg = parse_algorithm_option(options_item); - /* For aliases that imply a specific algorithm, default if not given. */ - if (alg == CRYPTO_ALG_UNKNOWN) { - if (strcmp(method, VERB_SSH_SIGN) == 0) { - alg = CRYPTO_ALG_ED25519; - } else if (strcmp(method, VERB_KEM_ENCAPS) == 0 || - strcmp(method, VERB_KEM_DECAPS) == 0) { - alg = CRYPTO_ALG_ML_KEM_768; - } - } - if (alg == CRYPTO_ALG_UNKNOWN) { return make_error_response(id_str, -32602, "missing_or_invalid_algorithm"); } @@ -943,7 +902,7 @@ static char *handle_algorithm_verb(dispatcher_ctx_t *ctx, const char *id_str, index = parse_index_option(options_item); /* Enforcement: check verb+algorithm validity. */ - enforce_rc = enforce_verb_algorithm(canonical, alg); + enforce_rc = enforce_verb_algorithm(method, alg); if (enforce_rc != ENFORCE_OK) { if (enforce_rc == ENFORCE_ERR_ALGORITHM) { return make_error_response(id_str, 1010, "algorithm_not_supported_for_verb"); @@ -968,7 +927,7 @@ static char *handle_algorithm_verb(dispatcher_ctx_t *ctx, const char *id_str, } /* ---- get_public_key (algorithm-based path) ---- */ - if (strcmp(canonical, VERB_GET_PUBLIC_KEY) == 0) { + if (strcmp(method, VERB_GET_PUBLIC_KEY) == 0) { if (alg_key_cache_derive(ctx->alg_key_cache, ctx->mnemonic, alg, index) != 0) { return make_error_response(id_str, -32602, "key_derivation_failed"); } @@ -987,7 +946,7 @@ static char *handle_algorithm_verb(dispatcher_ctx_t *ctx, const char *id_str, } /* ---- sign ---- */ - if (strcmp(canonical, VERB_SIGN) == 0) { + if (strcmp(method, VERB_SIGN) == 0) { cJSON *msg_hex_item = cJSON_GetArrayItem(params_item, 0); const char *msg_hex; unsigned char *msg_bytes = NULL; @@ -1091,7 +1050,7 @@ static char *handle_algorithm_verb(dispatcher_ctx_t *ctx, const char *id_str, } /* ---- verify ---- */ - if (strcmp(canonical, VERB_VERIFY) == 0) { + if (strcmp(method, VERB_VERIFY) == 0) { cJSON *msg_hex_item = cJSON_GetArrayItem(params_item, 0); cJSON *sig_hex_item = cJSON_GetArrayItem(params_item, 1); const char *msg_hex; @@ -1195,7 +1154,7 @@ static char *handle_algorithm_verb(dispatcher_ctx_t *ctx, const char *id_str, } /* ---- encapsulate (ML-KEM-768) ---- */ - if (strcmp(canonical, VERB_ENCAPSULATE) == 0) { + if (strcmp(method, VERB_ENCAPSULATE) == 0) { cJSON *pub_hex_item = cJSON_GetArrayItem(params_item, 0); const char *pub_hex; unsigned char *pub = NULL; @@ -1256,7 +1215,7 @@ static char *handle_algorithm_verb(dispatcher_ctx_t *ctx, const char *id_str, } /* ---- decapsulate (ML-KEM-768) ---- */ - if (strcmp(canonical, VERB_DECAPSULATE) == 0) { + if (strcmp(method, VERB_DECAPSULATE) == 0) { cJSON *ct_hex_item = cJSON_GetArrayItem(params_item, 0); const char *ct_hex; unsigned char *ct = NULL; @@ -1322,7 +1281,7 @@ static char *handle_algorithm_verb(dispatcher_ctx_t *ctx, const char *id_str, } /* ---- derive_shared_secret (x25519) ---- */ - if (strcmp(canonical, VERB_DERIVE_SHARED) == 0) { + if (strcmp(method, VERB_DERIVE_SHARED) == 0) { cJSON *peer_hex_item = cJSON_GetArrayItem(params_item, 0); const char *peer_hex; unsigned char *peer_pub = NULL; @@ -1396,21 +1355,21 @@ static char *handle_algorithm_verb(dispatcher_ctx_t *ctx, const char *id_str, /* ---- OTP verb handler ---- * - * Handles otp_encrypt and otp_decrypt. These verbs operate on the bound OTP - * pad (one pad per session) and do not use the role table or mnemonic. + * Handles encrypt/decrypt with algorithm:"otp". These verbs operate on the + * bound OTP pad (one pad per session) and do not use the role table or mnemonic. * - * Request shapes (see plans/otp_nostr_integration.md): - * otp_encrypt: params = [ "<plaintext-base64>", { "encoding": "ascii|binary" } ] - * otp_decrypt: params = [ "<ciphertext>", { "encoding": "ascii|binary" } ] + * Request shapes (see api.md §4.7): + * encrypt: params = [ "<plaintext-base64>", { "algorithm":"otp", "encoding": "ascii|binary" } ] + * decrypt: params = [ "<ciphertext>", { "algorithm":"otp", "encoding": "ascii|binary" } ] * - * The ciphertext for otp_decrypt is either an ASCII-armored string (encoding + * The ciphertext for decrypt is either an ASCII-armored string (encoding * "ascii") or a base64-encoded binary .otp blob (encoding "binary"). If * encoding is omitted, auto-detection by magic bytes is used. * * Results are returned as a JSON object string: - * otp_encrypt: {"ciphertext":"<ascii-armor-or-base64-blob>","encoding":"...", - * "pad_chksum":"<64hex>","pad_offset_before":N,"pad_offset_after":M} - * otp_decrypt: {"plaintext":"<base64>","encoding":"..."} + * encrypt: {"ciphertext":"<ascii-armor-or-base64-blob>","encoding":"...", + * "pad_chksum":"<64hex>","pad_offset_before":N,"pad_offset_after":M} + * decrypt: {"plaintext":"<base64>","encoding":"..."} */ static char *handle_otp_verb(const char *id_str, const char *method, cJSON *params_item, cJSON *options_item) { @@ -1433,7 +1392,7 @@ static char *handle_otp_verb(const char *id_str, const char *method, return make_error_response(id_str, -32601, "otp_pad_not_bound"); } - if (strcmp(method, VERB_OTP_ENCRYPT) == 0) { + if (strcmp(method, VERB_ENCRYPT) == 0) { /* Decode base64 plaintext. */ int pt_len = 0; unsigned char *pt = otppad_base64_decode(payload_item->valuestring, &pt_len); @@ -1499,7 +1458,7 @@ static char *handle_otp_verb(const char *id_str, const char *method, return make_error_response(id_str, -32602, "invalid_params"); } return make_success_response(id_str, out); - } else if (strcmp(method, VERB_OTP_DECRYPT) == 0) { + } else if (strcmp(method, VERB_DECRYPT) == 0) { unsigned char *pt = NULL; size_t pt_len = 0; int rc; @@ -1573,7 +1532,6 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request) char *owned_result = NULL; ptrdiff_t role_index_diff; int role_index; - crypto_alg_t alg; if (ctx == NULL || ctx->role_table == NULL || ctx->mnemonic == NULL || ctx->key_store == NULL || json_request == NULL) { return make_error_response("null", -32600, "invalid_request"); @@ -1614,88 +1572,39 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request) /* ---- Algorithm-based verb routing ---- * - * The new algorithm-based verbs (sign, verify, encapsulate, decapsulate, - * derive_shared_secret) and their aliases (sign_data, ssh_sign, - * verify_signature, kem_encapsulate, kem_decapsulate) are handled here, - * BEFORE the role-based selector resolution. They use the algorithm_key_cache - * for on-demand key derivation and do not need a role table entry. - * - * get_public_key is special: if the options contain an "algorithm" field, - * it takes the algorithm-based path; otherwise it falls through to the - * role-based path (backward compatibility). */ - /* New algorithm-based verbs (sign, verify, encapsulate, decapsulate, - * derive_shared_secret) always take the algorithm-based path. - * Old verb aliases (sign_data, ssh_sign, verify_signature, kem_encapsulate, - * kem_decapsulate) only take the algorithm-based path if the "algorithm" - * parameter is present; otherwise they fall through to the role-based - * path for backward compatibility. */ + * The algorithm-based verbs (sign, verify, encapsulate, decapsulate, + * derive_shared_secret, get_public_key) are handled here, BEFORE the + * role-based selector resolution. They use the algorithm_key_cache for + * on-demand key derivation and do not need a role table entry. Callers + * must supply `algorithm` explicitly. */ if (is_algorithm_verb(method)) { - int is_new_verb = (strcmp(method, VERB_SIGN) == 0 || - strcmp(method, VERB_VERIFY) == 0 || - strcmp(method, VERB_ENCAPSULATE) == 0 || - strcmp(method, VERB_DECAPSULATE) == 0 || - strcmp(method, VERB_DERIVE_SHARED) == 0); - int has_algorithm_opt = 0; - - if (cJSON_IsObject(options_item)) { - cJSON *alg_check = cJSON_GetObjectItemCaseSensitive(options_item, "algorithm"); - if (cJSON_IsString(alg_check) && alg_check->valuestring != NULL) { - has_algorithm_opt = 1; - } - } - - if (is_new_verb || has_algorithm_opt) { - char *alg_response = handle_algorithm_verb(ctx, id_str, method, - params_item, options_item); - cJSON_Delete(root); - return alg_response; - } - /* Old alias without algorithm= → fall through to role-based path */ - } - - if (strcmp(method, VERB_GET_PUBLIC_KEY) == 0 && cJSON_IsObject(options_item)) { - cJSON *alg_check = cJSON_GetObjectItemCaseSensitive(options_item, "algorithm"); - if (cJSON_IsString(alg_check) && alg_check->valuestring != NULL) { - /* Algorithm-based get_public_key path. */ - char *alg_response = handle_algorithm_verb(ctx, id_str, VERB_GET_PUBLIC_KEY, - params_item, options_item); - cJSON_Delete(root); - return alg_response; - } + char *alg_response = handle_algorithm_verb(ctx, id_str, method, + params_item, options_item); + cJSON_Delete(root); + return alg_response; } /* ---- OTP verb routing ---- * - * otp_encrypt and otp_decrypt do not use the role table or the mnemonic; - * they operate on the bound OTP pad (one pad per session, bound at - * startup via --otp-pad-dir / --otp-pad). Route them before role + * encrypt/decrypt with algorithm:"otp" do not use the role table or the + * mnemonic; they operate on the bound OTP pad (one pad per session, bound + * at startup via --otp-pad-dir / --otp-pad). Route them before role * resolution. */ - if (strcmp(method, VERB_OTP_ENCRYPT) == 0 || - strcmp(method, VERB_OTP_DECRYPT) == 0) { - char *otp_response = handle_otp_verb(id_str, method, params_item, options_item); - cJSON_Delete(root); - return otp_response; - } - - /* General encrypt/decrypt verbs: route to OTP if curve is "otp", - * otherwise fall through to the role-based path (for secp256k1 NIP-04/44, - * x25519, ml-kem-768). */ if (strcmp(method, VERB_ENCRYPT) == 0 || strcmp(method, VERB_DECRYPT) == 0) { if (cJSON_IsObject(options_item)) { - cJSON *curve_item = cJSON_GetObjectItemCaseSensitive(options_item, "curve"); - if (cJSON_IsString(curve_item) && curve_item->valuestring != NULL && - strcmp(curve_item->valuestring, "otp") == 0) { - /* Route to OTP handler — map encrypt→otp_encrypt, decrypt→otp_decrypt */ - const char *otp_method = (strcmp(method, VERB_ENCRYPT) == 0) - ? VERB_OTP_ENCRYPT : VERB_OTP_DECRYPT; - char *otp_response = handle_otp_verb(id_str, otp_method, + cJSON *alg_item = cJSON_GetObjectItemCaseSensitive(options_item, "algorithm"); + if (cJSON_IsString(alg_item) && alg_item->valuestring != NULL && + strcmp(alg_item->valuestring, "otp") == 0) { + char *otp_response = handle_otp_verb(id_str, method, params_item, options_item); cJSON_Delete(root); return otp_response; } } - /* For other curves, fall through to role-based path below. */ + /* Other algorithms: not valid for encrypt/decrypt in the new API. */ + cJSON_Delete(root); + return make_error_response(id_str, 1010, "algorithm_not_supported_for_verb"); } if (cJSON_IsObject(options_item)) { @@ -1774,64 +1683,19 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request) } role_index = (int)role_index_diff; - if (strcmp(method, VERB_GET_PUBLIC_KEY) == 0) { - /* get_public_key: returns the role's public key. + if (strcmp(method, VERB_NOSTR_GET_PUBLIC_KEY) == 0) { + /* nostr_get_public_key: returns the role's secp256k1 (NIP-06) public key. * - * For secp256k1 (backward compatibility): the result is a plain hex - * string (the existing format) unless the client requests the - * structured format via the options field {"format":"structured"}. - * - * For all other algorithms (ed25519, x25519, ML-DSA-65, - * SLH-DSA-128s, ML-KEM-768): the result is a structured JSON object + * By default the result is a plain 64-hex-char secp256k1 public key + * string. If the client requests the structured format via the options + * field {"format":"structured"}, the result is a structured JSON object * serialized as a string: - * {"algorithm":"<alg>","public_key":"<hex>","key_id":"<16 hex>"} + * {"algorithm":"secp256k1","public_key":"<hex>","key_id":"<16 hex>"} * * The key_id is the first 16 hex characters of the public key — a - * short display identifier (similar to an SSH key fingerprint). - * - * The full (untruncated) public key is read from the key_store via - * crypto_get_pubkey_hex(), because role->pubkey_hex is only - * ROLE_PUBKEY_HEX_MAX (66) bytes — too small for PQ pubkeys. + * short display identifier. */ - alg = crypto_alg_from_role(role->curve, role->purpose); - if (role->derived && alg != CRYPTO_ALG_SECP256K1 && - alg != CRYPTO_ALG_UNKNOWN) { - /* Non-secp256k1: always structured. */ - cJSON *pk_obj = cJSON_CreateObject(); - const char *alg_name; - const char *pub_hex = crypto_get_pubkey_hex(ctx->key_store, role_index); - char key_id[17]; - - if (pk_obj == NULL) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - - alg_name = crypto_alg_to_str(alg); - cJSON_AddStringToObject(pk_obj, "algorithm", - (alg_name != NULL) ? alg_name : "unknown"); - cJSON_AddStringToObject(pk_obj, "public_key", - (pub_hex != NULL) ? pub_hex : ""); - - /* key_id = first 16 hex chars of pubkey (or empty if too short). */ - if (pub_hex != NULL && strlen(pub_hex) >= 16) { - memcpy(key_id, pub_hex, 16); - key_id[16] = '\0'; - } else { - key_id[0] = '\0'; - } - cJSON_AddStringToObject(pk_obj, "key_id", key_id); - - owned_result = cJSON_PrintUnformatted(pk_obj); - cJSON_Delete(pk_obj); - if (owned_result == NULL) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - result = owned_result; - } else if (role->derived && alg == CRYPTO_ALG_SECP256K1) { - /* secp256k1: plain hex by default, structured if requested. - * role->pubkey_hex is large enough for secp256k1 (64 hex chars). */ + if (role->derived) { int want_structured = 0; cJSON *fmt_item = NULL; @@ -1871,14 +1735,14 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request) } result = owned_result; } else { - /* Backward-compatible plain hex string. */ + /* Plain hex string. */ result = role->pubkey_hex; } } else { - /* Not yet derived, or unknown algorithm. */ + /* Not yet derived. */ result = "not_yet_derived"; } - } else if (strcmp(method, VERB_SIGN_EVENT) == 0) { + } else if (strcmp(method, VERB_NOSTR_SIGN_EVENT) == 0) { cJSON *event_item = cJSON_GetArrayItem(params_item, 0); if (!cJSON_IsString(event_item) || event_item->valuestring == NULL) { cJSON_Delete(root); @@ -1891,7 +1755,7 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request) return make_error_response(id_str, -32602, "invalid_params"); } result = owned_result; - } else if (strcmp(method, VERB_MINE_EVENT) == 0) { + } else if (strcmp(method, VERB_NOSTR_MINE_EVENT) == 0) { cJSON *event_item = cJSON_GetArrayItem(params_item, 0); cJSON *diff_item = NULL; cJSON *threads_item = NULL; @@ -1945,7 +1809,7 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request) return make_error_response(id_str, 1008, "mining_failed"); } result = owned_result; - } else if (strcmp(method, VERB_NIP44_ENCRYPT) == 0) { + } else if (strcmp(method, VERB_NOSTR_NIP44_ENCRYPT) == 0) { cJSON *peer_item = cJSON_GetArrayItem(params_item, 0); cJSON *msg_item = cJSON_GetArrayItem(params_item, 1); if (!cJSON_IsString(peer_item) || peer_item->valuestring == NULL || @@ -1959,7 +1823,7 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request) return make_error_response(id_str, -32602, "invalid_params"); } result = owned_result; - } else if (strcmp(method, VERB_NIP44_DECRYPT) == 0) { + } else if (strcmp(method, VERB_NOSTR_NIP44_DECRYPT) == 0) { cJSON *peer_item = cJSON_GetArrayItem(params_item, 0); cJSON *msg_item = cJSON_GetArrayItem(params_item, 1); if (!cJSON_IsString(peer_item) || peer_item->valuestring == NULL || @@ -1973,7 +1837,7 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request) return make_error_response(id_str, -32602, "invalid_params"); } result = owned_result; - } else if (strcmp(method, VERB_NIP04_ENCRYPT) == 0) { + } else if (strcmp(method, VERB_NOSTR_NIP04_ENCRYPT) == 0) { cJSON *peer_item = cJSON_GetArrayItem(params_item, 0); cJSON *msg_item = cJSON_GetArrayItem(params_item, 1); if (!cJSON_IsString(peer_item) || peer_item->valuestring == NULL || @@ -1987,7 +1851,7 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request) return make_error_response(id_str, -32602, "invalid_params"); } result = owned_result; - } else if (strcmp(method, VERB_NIP04_DECRYPT) == 0) { + } else if (strcmp(method, VERB_NOSTR_NIP04_DECRYPT) == 0) { cJSON *peer_item = cJSON_GetArrayItem(params_item, 0); cJSON *msg_item = cJSON_GetArrayItem(params_item, 1); if (!cJSON_IsString(peer_item) || peer_item->valuestring == NULL || @@ -2001,479 +1865,11 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request) return make_error_response(id_str, -32602, "invalid_params"); } result = owned_result; - } else if (strcmp(method, VERB_ENCRYPT) == 0 || - strcmp(method, VERB_DECRYPT) == 0) { - /* General encrypt/decrypt verbs (role-based path for non-OTP curves). - * The curve is determined by the role's curve. For secp256k1, the - * nip_version option (4 or 44, default 44) selects NIP-04 or NIP-44. - * For x25519 and ml-kem-768, the key agreement is available but - * direct encrypt/decrypt is not yet implemented. */ - const char *curve_str = role->curve_str; - int nip_version = 44; /* default to NIP-44 */ - - if (cJSON_IsObject(options_item)) { - cJSON *nv = cJSON_GetObjectItemCaseSensitive(options_item, "nip_version"); - if (cJSON_IsNumber(nv)) { - nip_version = nv->valueint; - } - } - - if (role->curve == CURVE_SECP256K1) { - /* secp256k1: route to NIP-04 or NIP-44 */ - cJSON *peer_item = cJSON_GetArrayItem(params_item, 0); - cJSON *msg_item = cJSON_GetArrayItem(params_item, 1); - if (!cJSON_IsString(peer_item) || peer_item->valuestring == NULL || - !cJSON_IsString(msg_item) || msg_item->valuestring == NULL) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - if (strcmp(method, VERB_ENCRYPT) == 0) { - if (nip_version == 4) { - owned_result = crypto_nip04_encrypt(ctx->key_store, role_index, - peer_item->valuestring, - msg_item->valuestring); - } else { - owned_result = crypto_nip44_encrypt(ctx->key_store, role_index, - peer_item->valuestring, - msg_item->valuestring); - } - } else { /* decrypt */ - if (nip_version == 4) { - owned_result = crypto_nip04_decrypt(ctx->key_store, role_index, - peer_item->valuestring, - msg_item->valuestring); - } else { - owned_result = crypto_nip44_decrypt(ctx->key_store, role_index, - peer_item->valuestring, - msg_item->valuestring); - } - } - if (owned_result == NULL) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - result = owned_result; - } else if (role->curve == CURVE_X25519) { - /* x25519: ECDH + symmetric encryption — not yet implemented. - * The derive_shared_secret verb is available for key agreement; - * the caller can use the shared secret with their own symmetric - * cipher. Direct encrypt/decrypt will be added in a future release. */ - cJSON_Delete(root); - return make_error_response(id_str, -32601, - "encrypt_decrypt_not_implemented_for_x25519"); - } else if (role->curve == CURVE_ML_KEM_768) { - /* ml-kem-768: KEM-based hybrid encryption — not yet implemented. - * The encapsulate/decapsulate verbs are available for key agreement; - * direct encrypt/decrypt will be added in a future release. */ - cJSON_Delete(root); - return make_error_response(id_str, -32601, - "encrypt_decrypt_not_implemented_for_ml_kem_768"); - } else { - cJSON_Delete(root); - return make_error_response(id_str, -32602, - "encrypt_decrypt_not_supported_for_curve"); - } - } else if (strcmp(method, VERB_SIGN_DATA) == 0 || - strcmp(method, VERB_SSH_SIGN) == 0) { - /* sign_data / ssh_sign: params = [message_bytes_hex, options] - * Sign the decoded message bytes with the role's key. - * Returns {"signature":"<hex>","algorithm":"<alg_name>"}. - * Currently only ed25519 is implemented; PQ algorithms return - * not_yet_implemented. */ - cJSON *msg_hex_item = cJSON_GetArrayItem(params_item, 0); - const char *msg_hex; - unsigned char *msg_bytes = NULL; - size_t msg_len = 0; - const unsigned char *priv_key; - const crypto_alg_sizes_t *sz; - unsigned char sig[7856]; /* large enough for ed25519 (64), ML-DSA-65 (3309), SLH-DSA-128s (7856) */ - size_t sig_len = 0; - cJSON *result_obj = NULL; - const char *alg_name; - - if (!cJSON_IsString(msg_hex_item) || msg_hex_item->valuestring == NULL) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - msg_hex = msg_hex_item->valuestring; - - /* Get the derived key for this role. */ - priv_key = crypto_get_private_key(ctx->key_store, role_index); - if (priv_key == NULL) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "key_not_derived"); - } - - /* Determine the algorithm from the role. */ - alg = crypto_alg_from_role(role->curve, role->purpose); - sz = crypto_alg_get_sizes(alg); - if (sz == NULL) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - - /* Only ed25519, ML-DSA-65, and SLH-DSA-128s are implemented for sign_data. */ - if (alg != CRYPTO_ALG_ED25519 && alg != CRYPTO_ALG_ML_DSA_65 && - alg != CRYPTO_ALG_SLH_DSA_128S) { - cJSON_Delete(root); - return make_error_response(id_str, 1009, "not_yet_implemented"); - } - - /* Decode hex message to bytes. */ - msg_len = strlen(msg_hex) / 2; - if (msg_len == 0) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - msg_bytes = (unsigned char *)malloc(msg_len); - if (msg_bytes == NULL) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - if (nostr_hex_to_bytes(msg_hex, msg_bytes, msg_len) != 0) { - free(msg_bytes); - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - - sig_len = sz->sig_len; - if (alg == CRYPTO_ALG_ML_DSA_65) { - /* ML-DSA-65 signing (deterministic, FIPS 204) */ - if (crypto_ml_dsa_65_sign(priv_key, sz->priv_key_len, - msg_bytes, msg_len, - sig, &sig_len) != 0) { - free(msg_bytes); - cJSON_Delete(root); - return make_error_response(id_str, -32602, "signing_failed"); - } - } else if (alg == CRYPTO_ALG_SLH_DSA_128S) { - /* SLH-DSA-128s signing (deterministic, FIPS 205) */ - if (crypto_slh_dsa_128s_sign(priv_key, sz->priv_key_len, - msg_bytes, msg_len, - sig, &sig_len) != 0) { - free(msg_bytes); - cJSON_Delete(root); - return make_error_response(id_str, -32602, "signing_failed"); - } - } else { - /* ed25519 signing */ - if (crypto_ed25519_sign(priv_key, sz->priv_key_len, - msg_bytes, msg_len, - sig, &sig_len) != 0) { - free(msg_bytes); - cJSON_Delete(root); - return make_error_response(id_str, -32602, "signing_failed"); - } - } - free(msg_bytes); - - /* Build result JSON: {"signature":"<hex>","algorithm":"<alg_name>"} */ - result_obj = cJSON_CreateObject(); - if (result_obj == NULL) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - - { - char *sig_hex = (char *)malloc(sig_len * 2 + 1); - if (sig_hex == NULL) { - cJSON_Delete(result_obj); - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - nostr_bytes_to_hex(sig, sig_len, sig_hex); - cJSON_AddStringToObject(result_obj, "signature", sig_hex); - free(sig_hex); - } - - alg_name = crypto_alg_to_str(alg); - cJSON_AddStringToObject(result_obj, "algorithm", - (alg_name != NULL) ? alg_name : "unknown"); - - owned_result = cJSON_PrintUnformatted(result_obj); - cJSON_Delete(result_obj); - if (owned_result == NULL) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - result = owned_result; - } else if (strcmp(method, VERB_VERIFY_SIG) == 0) { - /* verify_signature: params = [message_bytes_hex, signature_hex, options] - * Verify the signature. Returns {"valid":true/false}. */ - cJSON *msg_hex_item = cJSON_GetArrayItem(params_item, 0); - cJSON *sig_hex_item = cJSON_GetArrayItem(params_item, 1); - const char *msg_hex; - const char *sig_hex; - unsigned char *msg_bytes = NULL; - unsigned char *sig_bytes = NULL; - size_t msg_len = 0; - size_t sig_len = 0; - const char *pub_hex; - unsigned char *pub = NULL; - size_t pub_hex_len; - const crypto_alg_sizes_t *sz; - int verify_rc; - cJSON *result_obj = NULL; - - if (!cJSON_IsString(msg_hex_item) || msg_hex_item->valuestring == NULL || - !cJSON_IsString(sig_hex_item) || sig_hex_item->valuestring == NULL) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - msg_hex = msg_hex_item->valuestring; - sig_hex = sig_hex_item->valuestring; - - alg = crypto_alg_from_role(role->curve, role->purpose); - sz = crypto_alg_get_sizes(alg); - if (sz == NULL) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - - if (alg != CRYPTO_ALG_ED25519 && alg != CRYPTO_ALG_ML_DSA_65 && - alg != CRYPTO_ALG_SLH_DSA_128S) { - cJSON_Delete(root); - return make_error_response(id_str, 1009, "not_yet_implemented"); - } - - /* Get the public key hex for this role. */ - pub_hex = crypto_get_pubkey_hex(ctx->key_store, role_index); - if (pub_hex == NULL) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "key_not_derived"); - } - pub_hex_len = strlen(pub_hex); - /* Check pub_hex length matches expected (2 * pub_key_len) */ - if (pub_hex_len != sz->pub_key_len * 2) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "key_not_derived"); - } - pub = (unsigned char *)malloc(sz->pub_key_len); - if (pub == NULL) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - if (nostr_hex_to_bytes(pub_hex, pub, sz->pub_key_len) != 0) { - free(pub); - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - - /* Decode message and signature from hex. */ - msg_len = strlen(msg_hex) / 2; - sig_len = strlen(sig_hex) / 2; - if (msg_len == 0 || sig_len == 0) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - msg_bytes = (unsigned char *)malloc(msg_len); - sig_bytes = (unsigned char *)malloc(sig_len); - if (msg_bytes == NULL || sig_bytes == NULL) { - free(msg_bytes); - free(sig_bytes); - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - if (nostr_hex_to_bytes(msg_hex, msg_bytes, msg_len) != 0 || - nostr_hex_to_bytes(sig_hex, sig_bytes, sig_len) != 0) { - free(msg_bytes); - free(sig_bytes); - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - - if (alg == CRYPTO_ALG_ML_DSA_65) { - verify_rc = crypto_ml_dsa_65_verify(pub, sz->pub_key_len, - msg_bytes, msg_len, - sig_bytes, sig_len); - } else if (alg == CRYPTO_ALG_SLH_DSA_128S) { - verify_rc = crypto_slh_dsa_128s_verify(pub, sz->pub_key_len, - msg_bytes, msg_len, - sig_bytes, sig_len); - } else { - verify_rc = crypto_ed25519_verify(pub, sz->pub_key_len, - msg_bytes, msg_len, - sig_bytes, sig_len); - } - free(pub); - free(msg_bytes); - free(sig_bytes); - - result_obj = cJSON_CreateObject(); - if (result_obj == NULL) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - cJSON_AddBoolToObject(result_obj, "valid", (verify_rc == 0) ? 1 : 0); - - owned_result = cJSON_PrintUnformatted(result_obj); - cJSON_Delete(result_obj); - if (owned_result == NULL) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - result = owned_result; - } else if (strcmp(method, VERB_KEM_ENCAPS) == 0) { - /* kem_encapsulate: params = [peer_pubkey_hex, options] - * Encapsulate with the peer's ML-KEM-768 public key. - * Returns {"ciphertext":"<hex>","shared_secret":"<hex>","algorithm":"ml-kem-768"} */ - cJSON *pub_hex_item = cJSON_GetArrayItem(params_item, 0); - const char *pub_hex; - unsigned char *pub = NULL; - unsigned char ct[1088]; - unsigned char ss[32]; - cJSON *result_obj = NULL; - const crypto_alg_sizes_t *sz; - - if (!cJSON_IsString(pub_hex_item) || pub_hex_item->valuestring == NULL) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - pub_hex = pub_hex_item->valuestring; - - alg = crypto_alg_from_role(role->curve, role->purpose); - sz = crypto_alg_get_sizes(alg); - if (sz == NULL || alg != CRYPTO_ALG_ML_KEM_768) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - - /* Decode hex public key (1184 bytes = 2368 hex chars) */ - if (strlen(pub_hex) != sz->pub_key_len * 2) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_pubkey_length"); - } - pub = (unsigned char *)malloc(sz->pub_key_len); - if (pub == NULL) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - if (nostr_hex_to_bytes(pub_hex, pub, sz->pub_key_len) != 0) { - free(pub); - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_pubkey_hex"); - } - - if (crypto_ml_kem_768_encaps(pub, sz->pub_key_len, ct, ss) != 0) { - free(pub); - cJSON_Delete(root); - return make_error_response(id_str, -32602, "encaps_failed"); - } - free(pub); - - /* Build result JSON */ - result_obj = cJSON_CreateObject(); - if (result_obj == NULL) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - { - char *ct_hex = (char *)malloc(sz->ciphertext_len * 2 + 1); - char *ss_hex = (char *)malloc(sz->shared_secret_len * 2 + 1); - if (ct_hex == NULL || ss_hex == NULL) { - free(ct_hex); free(ss_hex); - cJSON_Delete(result_obj); - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - nostr_bytes_to_hex(ct, sz->ciphertext_len, ct_hex); - nostr_bytes_to_hex(ss, sz->shared_secret_len, ss_hex); - cJSON_AddStringToObject(result_obj, "ciphertext", ct_hex); - cJSON_AddStringToObject(result_obj, "shared_secret", ss_hex); - free(ct_hex); free(ss_hex); - } - cJSON_AddStringToObject(result_obj, "algorithm", "ml-kem-768"); - - owned_result = cJSON_PrintUnformatted(result_obj); - cJSON_Delete(result_obj); - if (owned_result == NULL) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - result = owned_result; - } else if (strcmp(method, VERB_KEM_DECAPS) == 0) { - /* kem_decapsulate: params = [ciphertext_hex, options] - * Decapsulate using the role's ML-KEM-768 private key. - * Returns {"shared_secret":"<hex>","algorithm":"ml-kem-768"} */ - cJSON *ct_hex_item = cJSON_GetArrayItem(params_item, 0); - const char *ct_hex; - unsigned char *ct = NULL; - unsigned char ss[32]; - const unsigned char *priv_key; - const crypto_alg_sizes_t *sz; - cJSON *result_obj = NULL; - - if (!cJSON_IsString(ct_hex_item) || ct_hex_item->valuestring == NULL) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - ct_hex = ct_hex_item->valuestring; - - alg = crypto_alg_from_role(role->curve, role->purpose); - sz = crypto_alg_get_sizes(alg); - if (sz == NULL || alg != CRYPTO_ALG_ML_KEM_768) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - - /* Get the role's private key */ - priv_key = crypto_get_private_key(ctx->key_store, role_index); - if (priv_key == NULL) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "key_not_derived"); - } - - /* Decode hex ciphertext (1088 bytes = 2176 hex chars) */ - if (strlen(ct_hex) != sz->ciphertext_len * 2) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_ciphertext_length"); - } - ct = (unsigned char *)malloc(sz->ciphertext_len); - if (ct == NULL) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - if (nostr_hex_to_bytes(ct_hex, ct, sz->ciphertext_len) != 0) { - free(ct); - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_ciphertext_hex"); - } - - if (crypto_ml_kem_768_decaps(priv_key, sz->priv_key_len, - ct, sz->ciphertext_len, ss) != 0) { - free(ct); - cJSON_Delete(root); - return make_error_response(id_str, -32602, "decaps_failed"); - } - free(ct); - - /* Build result JSON */ - result_obj = cJSON_CreateObject(); - if (result_obj == NULL) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - { - char *ss_hex = (char *)malloc(sz->shared_secret_len * 2 + 1); - if (ss_hex == NULL) { - cJSON_Delete(result_obj); - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - nostr_bytes_to_hex(ss, sz->shared_secret_len, ss_hex); - cJSON_AddStringToObject(result_obj, "shared_secret", ss_hex); - free(ss_hex); - } - cJSON_AddStringToObject(result_obj, "algorithm", "ml-kem-768"); - - owned_result = cJSON_PrintUnformatted(result_obj); - cJSON_Delete(result_obj); - if (owned_result == NULL) { - cJSON_Delete(root); - return make_error_response(id_str, -32602, "invalid_params"); - } - result = owned_result; } else { + /* All remaining verbs (sign, verify, encapsulate, decapsulate, + * derive_shared_secret, get_public_key, encrypt, decrypt) are + * algorithm-based and handled before role resolution. Anything + * reaching here is unrecognized. */ cJSON_Delete(root); return make_error_response(id_str, -32601, "method_not_found"); } diff --git a/src/enforcement.c b/src/enforcement.c index c03101e..51bf56c 100644 --- a/src/enforcement.c +++ b/src/enforcement.c @@ -202,36 +202,25 @@ const char *selector_strerror(int err); #define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */ #define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */ -/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */ +/* Algorithm-based verb defines (algorithm is a parameter, not implicit). + * Callers must supply `algorithm` explicitly — no implicit defaults. */ #define VERB_SIGN "sign" #define VERB_VERIFY "verify" #define VERB_ENCAPSULATE "encapsulate" #define VERB_DECAPSULATE "decapsulate" #define VERB_DERIVE_SHARED "derive_shared_secret" +#define VERB_GET_PUBLIC_KEY "get_public_key" -#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */ +/* Nostr protocol verbs (secp256k1 NIP-06, role-based selector). */ +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_MINE_EVENT "nostr_mine_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" -/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */ -#define VERB_SIGN "sign" -#define VERB_VERIFY "verify" -#define VERB_ENCAPSULATE "encapsulate" -#define VERB_DECAPSULATE "decapsulate" -#define VERB_DERIVE_SHARED "derive_shared_secret" - - -/* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" -#define VERB_MINE_EVENT "mine_event" -#define VERB_SIGN_DATA "sign_data" -#define VERB_VERIFY_SIG "verify_signature" -#define VERB_SSH_SIGN "ssh_sign" -#define VERB_KEM_ENCAPS "kem_encapsulate" -#define VERB_KEM_DECAPS "kem_decapsulate" +/* OTP verbs (one-time pad; selected via algorithm:"otp"). */ #define VERB_ENCRYPT "encrypt" #define VERB_DECRYPT "decrypt" @@ -240,7 +229,7 @@ const char *selector_strerror(int err); * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. * * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: + * - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require: * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). */ @@ -712,69 +701,17 @@ int socket_name_random(char *out, size_t out_len); #include <string.h> +/* Check whether a verb is a Nostr protocol verb (role-based, secp256k1 NIP-06). */ static int is_nostr_verb(const char *verb) { if (verb == NULL) { return 0; } - - /* get_public_key is a universal verb — allowed for all algorithms. - * It is handled separately in enforce_verb_role() and excluded here. */ - return (strcmp(verb, VERB_SIGN_EVENT) == 0) || - (strcmp(verb, VERB_NIP44_ENCRYPT) == 0) || - (strcmp(verb, VERB_NIP44_DECRYPT) == 0) || - (strcmp(verb, VERB_NIP04_ENCRYPT) == 0) || - (strcmp(verb, VERB_NIP04_DECRYPT) == 0) || - (strcmp(verb, VERB_MINE_EVENT) == 0); -} - -static int is_sign_data_verb(const char *verb) { - if (verb == NULL) { - return 0; - } - return (strcmp(verb, VERB_SIGN_DATA) == 0) || - (strcmp(verb, VERB_VERIFY_SIG) == 0); -} - -static int is_kem_verb(const char *verb) { - if (verb == NULL) { - return 0; - } - return (strcmp(verb, VERB_KEM_ENCAPS) == 0) || - (strcmp(verb, VERB_KEM_DECAPS) == 0); -} - -/* Check whether a (purpose, curve) pair is allowed for sign_data / verify_signature. - * Returns ENFORCE_OK, ENFORCE_ERR_PURPOSE, or ENFORCE_ERR_CURVE. */ -static int enforce_sign_data_role(const role_entry_t *role) { - switch (role->purpose) { - case PURPOSE_SSH: - if (role->curve == CURVE_ED25519) { - return ENFORCE_OK; - } - return ENFORCE_ERR_CURVE; - - case PURPOSE_PQ_SIG: - if (role->curve == CURVE_ML_DSA_65 || - role->curve == CURVE_SLH_DSA_128S) { - return ENFORCE_OK; - } - return ENFORCE_ERR_CURVE; - - default: - return ENFORCE_ERR_PURPOSE; - } -} - -/* Check whether a (purpose, curve) pair is allowed for kem_encapsulate/decapsulate. - * Returns ENFORCE_OK, ENFORCE_ERR_PURPOSE, or ENFORCE_ERR_CURVE. */ -static int enforce_kem_role(const role_entry_t *role) { - if (role->purpose != PURPOSE_PQ_KEM) { - return ENFORCE_ERR_PURPOSE; - } - if (role->curve != CURVE_ML_KEM_768) { - return ENFORCE_ERR_CURVE; - } - return ENFORCE_OK; + return (strcmp(verb, VERB_NOSTR_SIGN_EVENT) == 0) || + (strcmp(verb, VERB_NOSTR_MINE_EVENT) == 0) || + (strcmp(verb, VERB_NOSTR_NIP44_ENCRYPT) == 0) || + (strcmp(verb, VERB_NOSTR_NIP44_DECRYPT) == 0) || + (strcmp(verb, VERB_NOSTR_NIP04_ENCRYPT) == 0) || + (strcmp(verb, VERB_NOSTR_NIP04_DECRYPT) == 0); } int enforce_verb_role(const char *verb, const role_entry_t *role) { @@ -782,14 +719,15 @@ int enforce_verb_role(const char *verb, const role_entry_t *role) { return ENFORCE_ERR_UNKNOWN_VERB; } - /* get_public_key is a universal verb — allowed for any role whose - * (curve, purpose) maps to a known algorithm via crypto_alg_from_role(). - * This lets clients retrieve public keys for all 6 algorithms. */ - if (strcmp(verb, VERB_GET_PUBLIC_KEY) == 0) { - crypto_alg_t alg = crypto_alg_from_role(role->curve, role->purpose); - if (alg == CRYPTO_ALG_UNKNOWN) { + /* nostr_get_public_key: returns the role's secp256k1 (NIP-06) public key. + * Requires PURPOSE_NOSTR + CURVE_SECP256K1. */ + if (strcmp(verb, VERB_NOSTR_GET_PUBLIC_KEY) == 0) { + if (role->purpose != PURPOSE_NOSTR) { return ENFORCE_ERR_PURPOSE; } + if (role->curve != CURVE_SECP256K1) { + return ENFORCE_ERR_CURVE; + } return ENFORCE_OK; } @@ -804,56 +742,25 @@ int enforce_verb_role(const char *verb, const role_entry_t *role) { return ENFORCE_OK; } - /* ssh_sign: PURPOSE_SSH + CURVE_ED25519 only. */ - if (strcmp(verb, VERB_SSH_SIGN) == 0) { - if (role->purpose != PURPOSE_SSH) { - return ENFORCE_ERR_PURPOSE; - } - if (role->curve != CURVE_ED25519) { - return ENFORCE_ERR_CURVE; - } - return ENFORCE_OK; - } - - /* sign_data / verify_signature: SSH+ed25519 or PQ-SIG algorithms. */ - if (is_sign_data_verb(verb)) { - return enforce_sign_data_role(role); - } - - /* kem_encapsulate / kem_decapsulate: PQ-KEM + ML-KEM-768 only. */ - if (is_kem_verb(verb)) { - return enforce_kem_role(role); - } - - /* encrypt / decrypt: allowed for secp256k1 (NIP-04/44), x25519 (age), - * and ml-kem-768 (pq-kem). OTP is handled before role resolution. */ - if (strcmp(verb, VERB_ENCRYPT) == 0 || strcmp(verb, VERB_DECRYPT) == 0) { - if (role->curve == CURVE_SECP256K1 && role->purpose == PURPOSE_NOSTR) { - return ENFORCE_OK; - } - if (role->curve == CURVE_X25519 && role->purpose == PURPOSE_AGE) { - return ENFORCE_OK; - } - if (role->curve == CURVE_ML_KEM_768 && role->purpose == PURPOSE_PQ_KEM) { - return ENFORCE_OK; - } - return ENFORCE_ERR_CURVE; - } - + /* All other verbs (sign, verify, encapsulate, decapsulate, + * derive_shared_secret, get_public_key, encrypt, decrypt) are + * algorithm-based and handled by enforce_verb_algorithm() before + * role resolution. Anything reaching here is unrecognized. */ return ENFORCE_ERR_UNKNOWN_VERB; } -/* ---- Algorithm-based enforcement (new) ---- +/* ---- Algorithm-based enforcement ---- * * enforce_verb_algorithm() checks whether a verb is valid for a given * algorithm, WITHOUT consulting the role table or purpose field. This is - * the enforcement path for the new algorithm-based verbs (sign, verify, + * the enforcement path for the algorithm-based verbs (sign, verify, * encapsulate, decapsulate, derive_shared_secret, get_public_key). * - * The Nostr-specific verbs (sign_event, nip44_*, nip04_*, mine_event) are - * always secp256k1 — that's a Nostr protocol requirement, not a policy - * choice. They are accepted here only when alg == CRYPTO_ALG_SECP256K1 - * so that algorithm-based policy entries can approve them too. + * The Nostr-specific verbs (nostr_sign_event, nostr_nip44_*, nostr_nip04_*, + * nostr_mine_event) are always secp256k1 — that's a Nostr protocol + * requirement, not a policy choice. They are accepted here only when + * alg == CRYPTO_ALG_SECP256K1 so that algorithm-based policy entries can + * approve them too. */ int enforce_verb_algorithm(const char *verb, crypto_alg_t alg) { if (verb == NULL) { @@ -896,16 +803,13 @@ int enforce_verb_algorithm(const char *verb, crypto_alg_t alg) { } /* Nostr verbs: always secp256k1 (protocol requirement). */ - if (is_nostr_verb(verb) || strcmp(verb, VERB_MINE_EVENT) == 0) { + if (is_nostr_verb(verb)) { if (alg == CRYPTO_ALG_SECP256K1) { return ENFORCE_OK; } return ENFORCE_ERR_ALGORITHM; } - /* Old verb aliases map to the new verbs and are handled by the - * dispatcher before reaching enforcement. If they reach here, - * treat them as unknown (the dispatcher should have remapped). */ return ENFORCE_ERR_UNKNOWN_VERB; } diff --git a/src/key_store.c b/src/key_store.c index f053970..4a4fcb7 100644 --- a/src/key_store.c +++ b/src/key_store.c @@ -212,18 +212,14 @@ const char *selector_strerror(int err); #define VERB_DERIVE_SHARED "derive_shared_secret" /* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" -#define VERB_MINE_EVENT "mine_event" -#define VERB_SIGN_DATA "sign_data" -#define VERB_VERIFY_SIG "verify_signature" -#define VERB_SSH_SIGN "ssh_sign" -#define VERB_KEM_ENCAPS "kem_encapsulate" -#define VERB_KEM_DECAPS "kem_decapsulate" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_MINE_EVENT "nostr_mine_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" /* * Check whether `verb` is allowed to execute against `role`. diff --git a/src/main.c b/src/main.c index 48a88da..1fbfeeb 100644 --- a/src/main.c +++ b/src/main.c @@ -226,19 +226,20 @@ const char *selector_strerror(int err); /* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" /* * Check whether `verb` is allowed to execute against `role`. * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. * * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: + * - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require: * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). */ @@ -759,8 +760,8 @@ int socket_name_random(char *out, size_t out_len); /* Version information (auto-updated by build/version tooling) */ #define NSIGNER_VERSION_MAJOR 0 #define NSIGNER_VERSION_MINOR 0 -#define NSIGNER_VERSION_PATCH 56 -#define NSIGNER_VERSION "v0.0.56" +#define NSIGNER_VERSION_PATCH 57 +#define NSIGNER_VERSION "v0.0.57" /* NSIGNER_HEADERLESS_DECLS_END */ diff --git a/src/mnemonic.c b/src/mnemonic.c index 79deb8f..1c863e0 100644 --- a/src/mnemonic.c +++ b/src/mnemonic.c @@ -220,19 +220,20 @@ const char *selector_strerror(int err); /* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" /* * Check whether `verb` is allowed to execute against `role`. * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. * * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: + * - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require: * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). */ diff --git a/src/otp_pad.c b/src/otp_pad.c index d39a642..9b97638 100644 --- a/src/otp_pad.c +++ b/src/otp_pad.c @@ -3,7 +3,7 @@ * * One pad per session (by design — see plans/otp_nostr_integration.md). * The pad is bound at startup via otp_pad_bind() and accessed by the - * otp_encrypt / otp_decrypt dispatcher verbs via otp_pad_get_state(). + * otp encrypt / otp decrypt dispatcher verbs via otp_pad_get_state(). * * The pad file is opened read-only and kept open for the lifetime of the * process. The per-pad .state file (offset counter) is read and written diff --git a/src/otp_pad.h b/src/otp_pad.h index 3aa9b77..b66c6df 100644 --- a/src/otp_pad.h +++ b/src/otp_pad.h @@ -1,8 +1,8 @@ /* * otp_pad.h — OTP pad state for n_signer (one pad per session). * - * Bound at startup via otp_pad_bind(); accessed by the otp_encrypt / - * otp_decrypt dispatcher verbs. See plans/otp_nostr_integration.md. + * Bound at startup via otp_pad_bind(); accessed by the otp encrypt / + * otp decrypt dispatcher verbs. See plans/otp_nostr_integration.md. */ #ifndef NSIGNER_OTP_PAD_H #define NSIGNER_OTP_PAD_H diff --git a/src/policy.c b/src/policy.c index cd3cc9d..cebf221 100644 --- a/src/policy.c +++ b/src/policy.c @@ -220,19 +220,20 @@ const char *selector_strerror(int err); /* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" /* * Check whether `verb` is allowed to execute against `role`. * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. * * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: + * - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require: * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). */ diff --git a/src/pq_crypto.c b/src/pq_crypto.c index 2500fb9..2175c6a 100644 --- a/src/pq_crypto.c +++ b/src/pq_crypto.c @@ -233,19 +233,20 @@ const char *selector_strerror(int err); /* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" /* * Check whether `verb` is allowed to execute against `role`. * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. * * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: + * - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require: * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). */ diff --git a/src/role_table.c b/src/role_table.c index 3c85eec..c280525 100644 --- a/src/role_table.c +++ b/src/role_table.c @@ -223,19 +223,20 @@ const char *selector_strerror(int err); /* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" /* * Check whether `verb` is allowed to execute against `role`. * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. * * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: + * - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require: * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). */ diff --git a/src/secure_mem.c b/src/secure_mem.c index e8e4dc3..0da0873 100644 --- a/src/secure_mem.c +++ b/src/secure_mem.c @@ -222,19 +222,20 @@ const char *selector_strerror(int err); /* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" /* * Check whether `verb` is allowed to execute against `role`. * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. * * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: + * - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require: * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). */ diff --git a/src/selector.c b/src/selector.c index b50ae12..60847ea 100644 --- a/src/selector.c +++ b/src/selector.c @@ -220,19 +220,20 @@ const char *selector_strerror(int err); /* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" /* * Check whether `verb` is allowed to execute against `role`. * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. * * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: + * - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require: * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). */ diff --git a/src/server.c b/src/server.c index 99c5982..621982a 100644 --- a/src/server.c +++ b/src/server.c @@ -226,19 +226,20 @@ const char *selector_strerror(int err); /* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" /* * Check whether `verb` is allowed to execute against `role`. * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. * * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: + * - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require: * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). */ @@ -1760,7 +1761,7 @@ int server_get_caller(int fd, caller_identity_t *out) { return 0; } -/* ---- Async mine_event support ---- */ +/* ---- Async nostr_mine_event support ---- */ typedef struct { int client_fd; @@ -2132,12 +2133,12 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) { if (pchk == POLICY_ALLOW && !derivation_error) { /* - * mine_event is a long-running operation (potentially seconds to minutes). + * nostr_mine_event is a long-running operation (potentially seconds to minutes). * Spawn a detached thread so the server stays responsive. The thread * owns the client_fd and request, and sends the response when mining * completes. We skip the synchronous response path below. */ - if (strcmp(method, "mine_event") == 0 && client_fd != STDIN_FILENO) { + if (strcmp(method, "nostr_mine_event") == 0 && client_fd != STDIN_FILENO) { mine_thread_arg_t *marg = malloc(sizeof(mine_thread_arg_t)); if (marg != NULL) { pthread_t mine_tid; diff --git a/src/socket_name.c b/src/socket_name.c index f814aad..d204eaa 100644 --- a/src/socket_name.c +++ b/src/socket_name.c @@ -222,19 +222,20 @@ const char *selector_strerror(int err); /* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" /* * Check whether `verb` is allowed to execute against `role`. * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. * * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: + * - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require: * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). */ diff --git a/tests/test.sh b/tests/test.sh index 6f9884e..31777c0 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -78,31 +78,31 @@ else fi echo -echo "[2/7] sign_event" +echo "[2/7] nostr_sign_event" now_ts="$(date +%s)" event_json='{"kind":1,"content":"hello from tests/test.sh","tags":[],"created_at":__TS__}' event_json="${event_json/__TS__/${now_ts}}" escaped_event_json="${event_json//\"/\\\"}" -req_sign="{\"id\":\"2\",\"method\":\"sign_event\",\"params\":[\"${escaped_event_json}\",{\"role\":\"main\"}]}" +req_sign="{\"id\":\"2\",\"method\":\"nostr_sign_event\",\"params\":[\"${escaped_event_json}\",{\"role\":\"main\"}]}" resp_sign="$(run_request "$req_sign")" echo "request : $req_sign" echo "response: $resp_sign" if printf '%s' "$resp_sign" | grep -q '"result"'; then - echo "[ok] sign_event returned result" + echo "[ok] nostr_sign_event returned result" else - echo "[warn] sign_event did not return result" + echo "[warn] nostr_sign_event did not return result" fi echo -echo "[3/7] sign_event with nostr_index=0" -req_sign_idx="{\"id\":\"3\",\"method\":\"sign_event\",\"params\":[\"${escaped_event_json}\",{\"nostr_index\":0}]}" +echo "[3/7] nostr_sign_event with nostr_index=0" +req_sign_idx="{\"id\":\"3\",\"method\":\"nostr_sign_event\",\"params\":[\"${escaped_event_json}\",{\"nostr_index\":0}]}" resp_sign_idx="$(run_request "$req_sign_idx")" echo "request : $req_sign_idx" echo "response: $resp_sign_idx" echo echo "[4/7] role selector error case (unknown role)" -req_bad_role='{"id":"4","method":"sign_event","params":["{}",{"role":"does_not_exist"}]}' +req_bad_role='{"id":"4","method":"nostr_nostr_sign_event","params":["{}",{"role":"does_not_exist"}]}' resp_bad_role="$(run_request "$req_bad_role")" echo "request : $req_bad_role" echo "response: $resp_bad_role" @@ -115,13 +115,13 @@ fi echo echo "[5/7] NIP-04 self round-trip" if [[ -n "$pubkey" ]]; then - req_nip04_enc="{\"id\":\"5\",\"method\":\"nip04_encrypt\",\"params\":[\"${pubkey}\",\"hello_nip04_from_test_sh\"]}" + req_nip04_enc="{\"id\":\"5\",\"method\":\"nostr_nostr_nip04_encrypt\",\"params\":[\"${pubkey}\",\"hello_nip04_from_test_sh\"]}" resp_nip04_enc="$(run_request "$req_nip04_enc")" echo "request : $req_nip04_enc" echo "response: $resp_nip04_enc" cipher_nip04="$(printf '%s' "$resp_nip04_enc" | extract_result_string)" if [[ -n "$cipher_nip04" ]]; then - req_nip04_dec="{\"id\":\"6\",\"method\":\"nip04_decrypt\",\"params\":[\"${pubkey}\",\"${cipher_nip04}\"]}" + req_nip04_dec="{\"id\":\"6\",\"method\":\"nostr_nostr_nip04_decrypt\",\"params\":[\"${pubkey}\",\"${cipher_nip04}\"]}" resp_nip04_dec="$(run_request "$req_nip04_dec")" echo "request : $req_nip04_dec" echo "response: $resp_nip04_dec" @@ -140,13 +140,13 @@ fi echo echo "[6/7] NIP-44 self round-trip" if [[ -n "$pubkey" ]]; then - req_nip44_enc="{\"id\":\"7\",\"method\":\"nip44_encrypt\",\"params\":[\"${pubkey}\",\"hello_nip44_from_test_sh\"]}" + req_nip44_enc="{\"id\":\"7\",\"method\":\"nostr_nostr_nip44_encrypt\",\"params\":[\"${pubkey}\",\"hello_nip44_from_test_sh\"]}" resp_nip44_enc="$(run_request "$req_nip44_enc")" echo "request : $req_nip44_enc" echo "response: $resp_nip44_enc" cipher_nip44="$(printf '%s' "$resp_nip44_enc" | extract_result_string)" if [[ -n "$cipher_nip44" ]]; then - req_nip44_dec="{\"id\":\"8\",\"method\":\"nip44_decrypt\",\"params\":[\"${pubkey}\",\"${cipher_nip44}\"]}" + req_nip44_dec="{\"id\":\"8\",\"method\":\"nostr_nostr_nip44_decrypt\",\"params\":[\"${pubkey}\",\"${cipher_nip44}\"]}" resp_nip44_dec="$(run_request "$req_nip44_dec")" echo "request : $req_nip44_dec" echo "response: $resp_nip44_dec" diff --git a/tests/test_algorithm_api.c b/tests/test_algorithm_api.c index e204c39..c34eddd 100644 --- a/tests/test_algorithm_api.c +++ b/tests/test_algorithm_api.c @@ -74,18 +74,14 @@ role_curve_t role_curve_from_str(const char *s); #define VERB_ENCAPSULATE "encapsulate" #define VERB_DECAPSULATE "decapsulate" #define VERB_DERIVE_SHARED "derive_shared_secret" -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" -#define VERB_MINE_EVENT "mine_event" -#define VERB_SIGN_DATA "sign_data" -#define VERB_VERIFY_SIG "verify_signature" -#define VERB_SSH_SIGN "ssh_sign" -#define VERB_KEM_ENCAPS "kem_encapsulate" -#define VERB_KEM_DECAPS "kem_decapsulate" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_MINE_EVENT "nostr_mine_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" int enforce_verb_role(const char *verb, const role_entry_t *role); const char *enforce_strerror(int err); @@ -369,10 +365,10 @@ int main(void) { enforce_verb_algorithm(VERB_DERIVE_SHARED, CRYPTO_ALG_ED25519) == ENFORCE_ERR_ALGORITHM); check("enforce get_public_key + any alg -> OK", enforce_verb_algorithm(VERB_GET_PUBLIC_KEY, CRYPTO_ALG_ED25519) == ENFORCE_OK); - check("enforce sign_event + secp256k1 -> OK", - enforce_verb_algorithm(VERB_SIGN_EVENT, CRYPTO_ALG_SECP256K1) == ENFORCE_OK); - check("enforce sign_event + ed25519 -> ALGORITHM err", - enforce_verb_algorithm(VERB_SIGN_EVENT, CRYPTO_ALG_ED25519) == ENFORCE_ERR_ALGORITHM); + check("enforce nostr_sign_event + secp256k1 -> OK", + enforce_verb_algorithm(VERB_NOSTR_SIGN_EVENT, CRYPTO_ALG_SECP256K1) == ENFORCE_OK); + check("enforce nostr_sign_event + ed25519 -> ALGORITHM err", + enforce_verb_algorithm(VERB_NOSTR_SIGN_EVENT, CRYPTO_ALG_ED25519) == ENFORCE_ERR_ALGORITHM); /* ---- get_public_key with algorithm parameter ---- */ resp = dispatcher_handle_request(&g_dispatcher, @@ -588,51 +584,51 @@ int main(void) { resp_has(resp, "algorithm_not_supported_for_verb") || resp_has(resp, "\"code\":1010")); free(resp); - /* ---- Old verb alias: sign_data with algorithm=ed25519 ---- */ + /* ---- Verb: sign with algorithm=ed25519 ---- */ resp = dispatcher_handle_request(&g_dispatcher, - "{\"id\":\"alias1\",\"method\":\"sign_data\",\"params\":[\"68656c6c6f\",{\"algorithm\":\"ed25519\",\"index\":0}]}"); - check("sign_data alias with algorithm=ed25519 returns result", + "{\"id\":\"alias1\",\"method\":\"sign\",\"params\":[\"68656c6c6f\",{\"algorithm\":\"ed25519\",\"index\":0}]}"); + check("sign with algorithm=ed25519 returns result", resp_has(resp, "\"result\"")); - check("sign_data alias has signature", + check("sign has signature", resp_has(resp, "signature")); free(resp); - /* ---- Old verb alias: ssh_sign with algorithm=ed25519 ---- */ + /* ---- Verb: sign (ssh) with algorithm=ed25519 ---- */ resp = dispatcher_handle_request(&g_dispatcher, - "{\"id\":\"alias2\",\"method\":\"ssh_sign\",\"params\":[\"68656c6c6f\",{\"algorithm\":\"ed25519\",\"index\":0}]}"); - check("ssh_sign alias with algorithm=ed25519 returns result", + "{\"id\":\"alias2\",\"method\":\"sign\",\"params\":[\"68656c6c6f\",{\"algorithm\":\"ed25519\",\"index\":0}]}"); + check("sign with algorithm=ed25519 returns result", resp_has(resp, "\"result\"")); free(resp); - /* ---- Old verb alias: kem_encapsulate with algorithm=ml-kem-768 ---- */ + /* ---- Verb: encapsulate with algorithm=ml-kem-768 ---- */ resp = dispatcher_handle_request(&g_dispatcher, "{\"id\":\"gpk5\",\"method\":\"get_public_key\",\"params\":[{\"algorithm\":\"ml-kem-768\",\"index\":1}]}"); pub_hex = extract_result_field(resp, "public_key"); free(resp); if (pub_hex) { snprintf(request, sizeof(request), - "{\"id\":\"alias3\",\"method\":\"kem_encapsulate\",\"params\":[\"%s\",{\"algorithm\":\"ml-kem-768\"}]}", + "{\"id\":\"alias3\",\"method\":\"encapsulate\",\"params\":[\"%s\",{\"algorithm\":\"ml-kem-768\"}]}", pub_hex); resp = dispatcher_handle_request(&g_dispatcher, request); - check("kem_encapsulate alias with algorithm=ml-kem-768 returns result", + check("encapsulate with algorithm=ml-kem-768 returns result", resp_has(resp, "\"result\"")); - check("kem_encapsulate alias has ciphertext", + check("encapsulate has ciphertext", resp_has(resp, "ciphertext")); free(resp); free(pub_hex); } - /* ---- Backward compat: sign_event with role still works ---- */ + /* ---- nostr_sign_event with role still works ---- */ resp = dispatcher_handle_request(&g_dispatcher, - "{\"id\":\"bc1\",\"method\":\"sign_event\",\"params\":[\"{\\\"kind\\\":1,\\\"content\\\":\\\"hello\\\",\\\"tags\\\":[]}\",{\"role\":\"main\"}]}"); - check("sign_event with role=main (backward compat) returns signed event", + "{\"id\":\"bc1\",\"method\":\"nostr_sign_event\",\"params\":[\"{\\\"kind\\\":1,\\\"content\\\":\\\"hello\\\",\\\"tags\\\":[]}\",{\"role\":\"main\"}]}"); + check("nostr_sign_event with role=main returns signed event", resp_has(resp, "pubkey") && resp_has(resp, "sig")); free(resp); - /* ---- Backward compat: get_public_key with role still works ---- */ + /* ---- nostr_get_public_key with role still works ---- */ resp = dispatcher_handle_request(&g_dispatcher, - "{\"id\":\"bc2\",\"method\":\"get_public_key\",\"params\":[{\"role\":\"main\"}]}"); - check("get_public_key with role=main (backward compat) returns hex", + "{\"id\":\"bc2\",\"method\":\"nostr_get_public_key\",\"params\":[{\"role\":\"main\"}]}"); + check("nostr_get_public_key with role=main returns hex", resp_has(resp, "\"result\"")); free(resp); diff --git a/tests/test_auth_envelope.c b/tests/test_auth_envelope.c index 1dc17ca..317c131 100644 --- a/tests/test_auth_envelope.c +++ b/tests/test_auth_envelope.c @@ -110,7 +110,7 @@ int main(void) { check_condition("nostr_crypto_init", nostr_crypto_init() == 0); auth_nonce_cache_init(&cache); - req = make_request_json(privkey, "req-1", "sign_event", (int)time(NULL)); + req = make_request_json(privkey, "req-1", "nostr_sign_event", (int)time(NULL)); check_condition("build valid request", req != NULL); if (req != NULL) { check_condition("valid auth envelope accepted", @@ -150,7 +150,7 @@ int main(void) { check_condition("parse second request", root != NULL); method_item = (root != NULL) ? cJSON_GetObjectItemCaseSensitive(root, "method") : NULL; if (method_item != NULL) { - cJSON_SetValuestring(method_item, "sign_event"); + cJSON_SetValuestring(method_item, "nostr_sign_event"); } tampered = (root != NULL) ? cJSON_PrintUnformatted(root) : NULL; cJSON_Delete(root); @@ -172,7 +172,7 @@ int main(void) { free(req); } - req = make_request_json(privkey, "req-3", "sign_event", (int)time(NULL)); + req = make_request_json(privkey, "req-3", "nostr_sign_event", (int)time(NULL)); check_condition("build replay request", req != NULL); if (req != NULL) { int first_ok = auth_envelope_verify_request(req, @@ -198,7 +198,7 @@ int main(void) { free(req); } - req = make_request_json(privkey, "req-4", "sign_event", (int)time(NULL) - 1000); + req = make_request_json(privkey, "req-4", "nostr_sign_event", (int)time(NULL) - 1000); check_condition("build stale request", req != NULL); if (req != NULL) { check_condition("stale request rejected", diff --git a/tests/test_dispatcher.c b/tests/test_dispatcher.c index 6c71150..f59195d 100644 --- a/tests/test_dispatcher.c +++ b/tests/test_dispatcher.c @@ -223,19 +223,20 @@ const char *selector_strerror(int err); /* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" /* * Check whether `verb` is allowed to execute against `role`. * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. * * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: + * - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require: * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). */ @@ -813,51 +814,51 @@ int main(void) { } } - /* 1. Valid get_public_key no selector -> default main, real pubkey */ + /* 1. Valid nostr_get_public_key no selector -> default main, real pubkey */ resp = dispatcher_handle_request(&dispatcher, - "{\"id\":\"1\",\"method\":\"get_public_key\",\"params\":[\"\"]}"); - check_condition("get_public_key default role returns derived hex", + "{\"id\":\"1\",\"method\":\"nostr_get_public_key\",\"params\":[\"\"]}"); + check_condition("nostr_get_public_key default role returns derived hex", response_has(resp, "\"id\":\"1\"") && !response_has(resp, "not_yet_derived") && response_has(resp, "\"result\":\"")); free(resp); - /* 2. sign_event with role main */ + /* 2. nostr_sign_event with role main */ resp = dispatcher_handle_request(&dispatcher, - "{\"id\":\"2\",\"method\":\"sign_event\",\"params\":[\"{\\\"kind\\\":1,\\\"content\\\":\\\"hello\\\",\\\"tags\\\":[]}\",{\"role\":\"main\"}]}"); - check_condition("sign_event with role=main returns signed event", + "{\"id\":\"2\",\"method\":\"nostr_sign_event\",\"params\":[\"{\\\"kind\\\":1,\\\"content\\\":\\\"hello\\\",\\\"tags\\\":[]}\",{\"role\":\"main\"}]}"); + check_condition("nostr_sign_event with role=main returns signed event", response_has(resp, "\"id\":\"2\"") && response_has(resp, "pubkey") && response_has(resp, "sig") && response_has(resp, "created_at")); free(resp); - /* 3. sign_event with nostr_index 0 */ + /* 3. nostr_sign_event with nostr_index 0 */ resp = dispatcher_handle_request(&dispatcher, - "{\"id\":\"3\",\"method\":\"sign_event\",\"params\":[\"{\\\"kind\\\":1,\\\"content\\\":\\\"hello2\\\",\\\"tags\\\":[]}\",{\"nostr_index\":0}]}"); - check_condition("sign_event with nostr_index=0 returns signed event", + "{\"id\":\"3\",\"method\":\"nostr_sign_event\",\"params\":[\"{\\\"kind\\\":1,\\\"content\\\":\\\"hello2\\\",\\\"tags\\\":[]}\",{\"nostr_index\":0}]}"); + check_condition("nostr_sign_event with nostr_index=0 returns signed event", response_has(resp, "\"id\":\"3\"") && response_has(resp, "pubkey") && response_has(resp, "sig") && response_has(resp, "created_at")); free(resp); /* 4. ambiguous selector role + nostr_index */ resp = dispatcher_handle_request(&dispatcher, - "{\"id\":\"4\",\"method\":\"sign_event\",\"params\":[\"{}\",{\"role\":\"main\",\"nostr_index\":0}]}"); + "{\"id\":\"4\",\"method\":\"nostr_sign_event\",\"params\":[\"{}\",{\"role\":\"main\",\"nostr_index\":0}]}"); check_condition("ambiguous selector returns 1001", response_has(resp, "\"id\":\"4\"") && response_has(resp, "\"code\":1001")); free(resp); /* 5. unknown role */ resp = dispatcher_handle_request(&dispatcher, - "{\"id\":\"5\",\"method\":\"sign_event\",\"params\":[\"{}\",{\"role\":\"nonexistent\"}]}"); + "{\"id\":\"5\",\"method\":\"nostr_sign_event\",\"params\":[\"{}\",{\"role\":\"nonexistent\"}]}"); check_condition("unknown role returns 1002 with unknown_role", response_has(resp, "\"id\":\"5\"") && response_has(resp, "\"code\":1002") && response_has(resp, "unknown_role")); free(resp); - /* 6. purpose mismatch: sign_event against ssh role */ + /* 6. purpose mismatch: nostr_sign_event against ssh role */ resp = dispatcher_handle_request(&dispatcher, - "{\"id\":\"6\",\"method\":\"sign_event\",\"params\":[\"{}\",{\"role\":\"ssh_key\"}]}"); + "{\"id\":\"6\",\"method\":\"nostr_sign_event\",\"params\":[\"{}\",{\"role\":\"ssh_key\"}]}"); check_condition("purpose mismatch returns 1004", response_has(resp, "\"id\":\"6\"") && response_has(resp, "\"code\":1004")); free(resp); /* 7. invalid JSON */ resp = dispatcher_handle_request(&dispatcher, - "{\"id\":\"7\",\"method\":\"sign_event\",\"params\":[\"{}\"]"); + "{\"id\":\"7\",\"method\":\"nostr_sign_event\",\"params\":[\"{}\"]"); check_condition("invalid JSON returns -32700", response_has(resp, "\"code\":-32700")); free(resp); @@ -872,7 +873,7 @@ int main(void) { /* 9. mnemonic not loaded */ mnemonic_unload(&mnemonic); resp = dispatcher_handle_request(&dispatcher, - "{\"id\":\"9\",\"method\":\"get_public_key\",\"params\":[\"\"]}"); + "{\"id\":\"9\",\"method\":\"nostr_get_public_key\",\"params\":[\"\"]}"); check_condition("mnemonic not loaded returns 1006", response_has(resp, "\"id\":\"9\"") && response_has(resp, "\"code\":1006")); free(resp); diff --git a/tests/test_ed25519_x25519.c b/tests/test_ed25519_x25519.c index 1668db3..914ef82 100644 --- a/tests/test_ed25519_x25519.c +++ b/tests/test_ed25519_x25519.c @@ -9,11 +9,11 @@ * - x25519 ECDH roundtrip: two sides derive matching shared secret * - x25519 determinism: same seed -> same keypair * - SLIP-0010 derivation: deterministic seed from mnemonic+path - * - Integration: derive ed25519 key via crypto_derive_one, sign_data via + * - Integration: derive ed25519 key via crypto_derive_one, sign via * dispatcher, verify signature - * - Enforcement: sign_data allowed on ssh+ed25519, rejected on nostr+secp256k1 - * - Enforcement: ssh_sign allowed on ssh+ed25519 - * - Enforcement: kem_encapsulate/decapsulate rejected on ssh/ed25519 + * - Enforcement: sign allowed on ssh+ed25519, rejected on nostr+secp256k1 + * - Enforcement: sign allowed on ssh+ed25519 + * - Enforcement: encapsulate/decapsulate rejected on ssh/ed25519 */ /* NSIGNER_HEADERLESS_DECLS_BEGIN */ #include <stddef.h> @@ -150,19 +150,21 @@ const char *selector_strerror(int err); #define ENFORCE_ERR_PURPOSE -1 #define ENFORCE_ERR_CURVE -2 #define ENFORCE_ERR_UNKNOWN_VERB -3 +#define ENFORCE_ERR_ALGORITHM -4 -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" -#define VERB_MINE_EVENT "mine_event" -#define VERB_SIGN_DATA "sign_data" -#define VERB_VERIFY_SIG "verify_signature" -#define VERB_SSH_SIGN "ssh_sign" -#define VERB_KEM_ENCAPS "kem_encapsulate" -#define VERB_KEM_DECAPS "kem_decapsulate" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_SIGN "sign" +#define VERB_VERIFY "verify" +#define VERB_ENCAPSULATE "encapsulate" +#define VERB_DECAPSULATE "decapsulate" +#define VERB_DERIVE_SHARED "derive_shared_secret" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_MINE_EVENT "nostr_mine_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" int enforce_verb_role(const char *verb, const role_entry_t *role); const char *enforce_strerror(int err); @@ -692,7 +694,7 @@ int main(void) { mnemonic_unload(&mnemonic_state); } - /* ---- Integration: sign_data via dispatcher ---- */ + /* ---- Integration: sign via dispatcher ---- */ { role_table_t table; role_entry_t ssh_role; @@ -717,27 +719,27 @@ int main(void) { dispatcher_init(&dispatcher, &table, &mnemonic_state, &key_store, &g_alg_key_cache); - /* sign_data request */ + /* sign request */ snprintf(request, sizeof(request), - "{\"id\":\"test1\",\"method\":\"sign_data\",\"params\":[\"%s\",{\"role\":\"ssh_main\"}]}", + "{\"id\":\"test1\",\"method\":\"sign\",\"params\":[\"%s\",{\"algorithm\":\"ed25519\",\"index\":0}]}", msg_hex); resp = dispatcher_handle_request(&dispatcher, request); - check_condition("sign_data via dispatcher returns result", + check_condition("sign via dispatcher returns result", resp != NULL && response_has(resp, "\"result\"")); - check_condition("sign_data result contains signature", + check_condition("sign result contains signature", resp != NULL && response_has(resp, "signature")); - check_condition("sign_data result contains algorithm ed25519", + check_condition("sign result contains algorithm ed25519", resp != NULL && response_has(resp, "ed25519")); free(resp); - /* ssh_sign request */ + /* sign request */ snprintf(request, sizeof(request), - "{\"id\":\"test2\",\"method\":\"ssh_sign\",\"params\":[\"%s\",{\"role\":\"ssh_main\"}]}", + "{\"id\":\"test2\",\"method\":\"sign\",\"params\":[\"%s\",{\"algorithm\":\"ed25519\",\"index\":0}]}", msg_hex); resp = dispatcher_handle_request(&dispatcher, request); - check_condition("ssh_sign via dispatcher returns result", + check_condition("sign via dispatcher returns result", resp != NULL && response_has(resp, "\"result\"")); - check_condition("ssh_sign result contains signature", + check_condition("sign result contains signature", resp != NULL && response_has(resp, "signature")); free(resp); @@ -745,38 +747,39 @@ int main(void) { mnemonic_unload(&mnemonic_state); } - /* ---- Enforcement: sign_data on ssh+ed25519 ---- */ + /* ---- Enforcement: sign on ssh+ed25519 ---- */ { role_entry_t ssh_ed = make_ssh_ed25519_entry("ssh", 0); role_entry_t nostr_secp = make_nostr_secp_entry("nostr", 0); role_entry_t pq_kem = make_pq_kem_entry("kem", 0); - check_condition("enforce sign_data + ssh/ed25519 -> OK", - enforce_verb_role(VERB_SIGN_DATA, &ssh_ed) == ENFORCE_OK); + /* sign/verify are algorithm-based now: checked via enforce_verb_algorithm(). */ + check_condition("enforce sign + ed25519 -> OK", + enforce_verb_algorithm(VERB_SIGN, CRYPTO_ALG_ED25519) == ENFORCE_OK); - check_condition("enforce sign_data + nostr/secp256k1 -> PURPOSE err", - enforce_verb_role(VERB_SIGN_DATA, &nostr_secp) == ENFORCE_ERR_PURPOSE); + check_condition("enforce sign + secp256k1 -> OK", + enforce_verb_algorithm(VERB_SIGN, CRYPTO_ALG_SECP256K1) == ENFORCE_OK); - check_condition("enforce ssh_sign + ssh/ed25519 -> OK", - enforce_verb_role(VERB_SSH_SIGN, &ssh_ed) == ENFORCE_OK); + check_condition("enforce verify + ed25519 -> OK", + enforce_verb_algorithm(VERB_VERIFY, CRYPTO_ALG_ED25519) == ENFORCE_OK); - check_condition("enforce ssh_sign + nostr/secp256k1 -> PURPOSE err", - enforce_verb_role(VERB_SSH_SIGN, &nostr_secp) == ENFORCE_ERR_PURPOSE); + check_condition("enforce sign + ml-kem-768 -> ALGORITHM err", + enforce_verb_algorithm(VERB_SIGN, CRYPTO_ALG_ML_KEM_768) == ENFORCE_ERR_ALGORITHM); - check_condition("enforce sign_event + ssh/ed25519 -> PURPOSE err", - enforce_verb_role(VERB_SIGN_EVENT, &ssh_ed) == ENFORCE_ERR_PURPOSE); + check_condition("enforce nostr_sign_event + ssh/ed25519 -> PURPOSE err", + enforce_verb_role(VERB_NOSTR_SIGN_EVENT, &ssh_ed) == ENFORCE_ERR_PURPOSE); - check_condition("enforce kem_encapsulate + pq-kem/ml-kem-768 -> OK", - enforce_verb_role(VERB_KEM_ENCAPS, &pq_kem) == ENFORCE_OK); + check_condition("enforce encapsulate + ml-kem-768 -> OK", + enforce_verb_algorithm(VERB_ENCAPSULATE, CRYPTO_ALG_ML_KEM_768) == ENFORCE_OK); - check_condition("enforce kem_decapsulate + pq-kem/ml-kem-768 -> OK", - enforce_verb_role(VERB_KEM_DECAPS, &pq_kem) == ENFORCE_OK); + check_condition("enforce decapsulate + ml-kem-768 -> OK", + enforce_verb_algorithm(VERB_DECAPSULATE, CRYPTO_ALG_ML_KEM_768) == ENFORCE_OK); - check_condition("enforce kem_encapsulate + ssh/ed25519 -> PURPOSE err", - enforce_verb_role(VERB_KEM_ENCAPS, &ssh_ed) == ENFORCE_ERR_PURPOSE); + check_condition("enforce encapsulate + ed25519 -> ALGORITHM err", + enforce_verb_algorithm(VERB_ENCAPSULATE, CRYPTO_ALG_ED25519) == ENFORCE_ERR_ALGORITHM); } - /* ---- Dispatcher: kem_encapsulate with invalid pubkey length ---- */ + /* ---- Dispatcher: encapsulate with invalid pubkey length ---- */ { role_table_t table; role_entry_t kem_role; @@ -784,7 +787,7 @@ int main(void) { key_store_t key_store; dispatcher_ctx_t dispatcher; char *resp; - const char *request = "{\"id\":\"k1\",\"method\":\"kem_encapsulate\",\"params\":[\"00\",{\"role\":\"kem_main\"}]}"; + const char *request = "{\"id\":\"k1\",\"method\":\"encapsulate\",\"params\":[\"00\",{\"algorithm\":\"ml-kem-768\",\"index\":0}]}"; role_table_init(&table); kem_role = make_pq_kem_entry("kem_main", 0); @@ -798,11 +801,11 @@ int main(void) { alg_key_cache_init(&g_alg_key_cache); dispatcher_init(&dispatcher, &table, &mnemonic_state, &key_store, &g_alg_key_cache); - /* kem_encapsulate is now implemented (Phase 5). With a too-short + /* encapsulate is now implemented (Phase 5). With a too-short * pubkey hex ("00" = 1 byte, but ML-KEM-768 needs 1184 bytes), * the dispatcher should return an invalid_pubkey_length error. */ resp = dispatcher_handle_request(&dispatcher, request); - check_condition("kem_encapsulate with short pubkey returns error", + check_condition("encapsulate with short pubkey returns error", resp != NULL && response_has(resp, "\"error\"")); free(resp); @@ -810,7 +813,7 @@ int main(void) { mnemonic_unload(&mnemonic_state); } - /* ---- Dispatcher: verify_signature roundtrip ---- */ + /* ---- Dispatcher: verify roundtrip ---- */ { role_table_t table; role_entry_t ssh_role; @@ -839,10 +842,10 @@ int main(void) { /* Sign */ snprintf(sign_req, sizeof(sign_req), - "{\"id\":\"s1\",\"method\":\"sign_data\",\"params\":[\"%s\",{\"role\":\"ssh_main\"}]}", + "{\"id\":\"s1\",\"method\":\"sign\",\"params\":[\"%s\",{\"algorithm\":\"ed25519\",\"index\":0}]}", msg_hex); sign_resp = dispatcher_handle_request(&dispatcher, sign_req); - check_condition("verify roundtrip: sign_data succeeds", sign_resp != NULL); + check_condition("verify roundtrip: sign succeeds", sign_resp != NULL); /* Extract signature from result */ if (sign_resp != NULL) { @@ -867,28 +870,28 @@ int main(void) { /* Verify */ if (sig_str != NULL) { snprintf(verify_req, sizeof(verify_req), - "{\"id\":\"v1\",\"method\":\"verify_signature\",\"params\":[\"%s\",\"%s\",{\"role\":\"ssh_main\"}]}", + "{\"id\":\"v1\",\"method\":\"verify\",\"params\":[\"%s\",\"%s\",{\"algorithm\":\"ed25519\",\"index\":0}]}", msg_hex, sig_str); verify_resp = dispatcher_handle_request(&dispatcher, verify_req); - check_condition("verify_signature returns valid:true", + check_condition("verify returns valid:true", verify_resp != NULL && response_has(verify_resp, "valid") && response_has(verify_resp, "true")); free(verify_resp); /* Verify with wrong message */ snprintf(verify_req, sizeof(verify_req), - "{\"id\":\"v2\",\"method\":\"verify_signature\",\"params\":[\"00ff\",\"%s\",{\"role\":\"ssh_main\"}]}", + "{\"id\":\"v2\",\"method\":\"verify\",\"params\":[\"00ff\",\"%s\",{\"algorithm\":\"ed25519\",\"index\":0}]}", sig_str); verify_resp = dispatcher_handle_request(&dispatcher, verify_req); - check_condition("verify_signature wrong msg returns valid:false", + check_condition("verify wrong msg returns valid:false", verify_resp != NULL && response_has(verify_resp, "valid") && response_has(verify_resp, "false")); free(verify_resp); free((void *)sig_str); } else { - check_condition("verify_signature returns valid:true", 0); - check_condition("verify_signature wrong msg returns valid:false", 0); + check_condition("verify returns valid:true", 0); + check_condition("verify wrong msg returns valid:false", 0); } free(sign_resp); diff --git a/tests/test_enforcement.c b/tests/test_enforcement.c index b5f32db..519af99 100644 --- a/tests/test_enforcement.c +++ b/tests/test_enforcement.c @@ -220,19 +220,20 @@ const char *selector_strerror(int err); /* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" /* * Check whether `verb` is allowed to execute against `role`. * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. * * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: + * - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require: * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). */ @@ -739,43 +740,43 @@ int main(void) { role_entry_t age_x = make_role("age", "x25519"); check_condition( - "sign_event + nostr/secp256k1 -> ENFORCE_OK", - enforce_verb_role(VERB_SIGN_EVENT, &nostr_secp) == ENFORCE_OK + "nostr_sign_event + nostr/secp256k1 -> ENFORCE_OK", + enforce_verb_role(VERB_NOSTR_SIGN_EVENT, &nostr_secp) == ENFORCE_OK ); check_condition( - "get_public_key + nostr/secp256k1 -> ENFORCE_OK", - enforce_verb_role(VERB_GET_PUBLIC_KEY, &nostr_secp) == ENFORCE_OK + "nostr_get_public_key + nostr/secp256k1 -> ENFORCE_OK", + enforce_verb_role(VERB_NOSTR_GET_PUBLIC_KEY, &nostr_secp) == ENFORCE_OK ); check_condition( - "nip44_encrypt + nostr/secp256k1 -> ENFORCE_OK", - enforce_verb_role(VERB_NIP44_ENCRYPT, &nostr_secp) == ENFORCE_OK + "nostr_nip44_encrypt + nostr/secp256k1 -> ENFORCE_OK", + enforce_verb_role(VERB_NOSTR_NIP44_ENCRYPT, &nostr_secp) == ENFORCE_OK ); check_condition( - "nip44_decrypt + nostr/secp256k1 -> ENFORCE_OK", - enforce_verb_role(VERB_NIP44_DECRYPT, &nostr_secp) == ENFORCE_OK + "nostr_nip44_decrypt + nostr/secp256k1 -> ENFORCE_OK", + enforce_verb_role(VERB_NOSTR_NIP44_DECRYPT, &nostr_secp) == ENFORCE_OK ); check_condition( - "nip04_encrypt + nostr/secp256k1 -> ENFORCE_OK", - enforce_verb_role(VERB_NIP04_ENCRYPT, &nostr_secp) == ENFORCE_OK + "nostr_nip04_encrypt + nostr/secp256k1 -> ENFORCE_OK", + enforce_verb_role(VERB_NOSTR_NIP04_ENCRYPT, &nostr_secp) == ENFORCE_OK ); check_condition( - "sign_event + bitcoin/secp256k1 -> ENFORCE_ERR_PURPOSE", - enforce_verb_role(VERB_SIGN_EVENT, &bitcoin_secp) == ENFORCE_ERR_PURPOSE + "nostr_sign_event + bitcoin/secp256k1 -> ENFORCE_ERR_PURPOSE", + enforce_verb_role(VERB_NOSTR_SIGN_EVENT, &bitcoin_secp) == ENFORCE_ERR_PURPOSE ); check_condition( - "sign_event + nostr/ed25519 -> ENFORCE_ERR_CURVE", - enforce_verb_role(VERB_SIGN_EVENT, &nostr_ed) == ENFORCE_ERR_CURVE + "nostr_sign_event + nostr/ed25519 -> ENFORCE_ERR_CURVE", + enforce_verb_role(VERB_NOSTR_SIGN_EVENT, &nostr_ed) == ENFORCE_ERR_CURVE ); check_condition( - "sign_event + ssh/ed25519 -> ENFORCE_ERR_PURPOSE", - enforce_verb_role(VERB_SIGN_EVENT, &ssh_ed) == ENFORCE_ERR_PURPOSE + "nostr_sign_event + ssh/ed25519 -> ENFORCE_ERR_PURPOSE", + enforce_verb_role(VERB_NOSTR_SIGN_EVENT, &ssh_ed) == ENFORCE_ERR_PURPOSE ); check_condition( @@ -784,8 +785,8 @@ int main(void) { ); check_condition( - "nip44_encrypt + age/x25519 -> ENFORCE_ERR_PURPOSE", - enforce_verb_role(VERB_NIP44_ENCRYPT, &age_x) == ENFORCE_ERR_PURPOSE + "nostr_nip44_encrypt + age/x25519 -> ENFORCE_ERR_PURPOSE", + enforce_verb_role(VERB_NOSTR_NIP44_ENCRYPT, &age_x) == ENFORCE_ERR_PURPOSE ); printf("%d/10 tests passed\n", g_passes); diff --git a/tests/test_integration.c b/tests/test_integration.c index ed90c37..5e046cb 100644 --- a/tests/test_integration.c +++ b/tests/test_integration.c @@ -222,19 +222,20 @@ const char *selector_strerror(int err); /* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" /* * Check whether `verb` is allowed to execute against `role`. * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. * * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: + * - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require: * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). */ @@ -1001,13 +1002,13 @@ int main(void) { nsigner_client_free(c); c = NULL; - /* sign_event (fresh connection) */ + /* nostr_sign_event (fresh connection) */ t = nsigner_transport_open_unix(SOCKET_NAME_A, 5000); if (t != NULL) { c = nsigner_client_new(t); if (c == NULL) { t->close(t); - check_condition("connect signer socket for sign_event", 0); + check_condition("connect signer socket for nostr_sign_event", 0); } else { params = cJSON_CreateArray(); if (params != NULL) { @@ -1018,10 +1019,10 @@ int main(void) { cJSON_AddItemToArray(params, opts); opts = NULL; } - if (nsigner_client_call(c, "sign_event", params, &se_result) == NOSTR_SUCCESS) { - check_condition("sign_event has result", se_result != NULL); + if (nsigner_client_call(c, "nostr_sign_event", params, &se_result) == NOSTR_SUCCESS) { + check_condition("nostr_sign_event has result", se_result != NULL); } else { - check_condition("sign_event request roundtrip", 0); + check_condition("nostr_sign_event request roundtrip", 0); } params = NULL; /* nsigner_client_call took ownership */ cJSON_Delete(se_result); @@ -1029,7 +1030,7 @@ int main(void) { } } } else { - check_condition("connect signer socket for sign_event", 0); + check_condition("connect signer socket for nostr_sign_event", 0); } cJSON_Delete(params); @@ -1052,7 +1053,7 @@ int main(void) { if (p) { p += 10; strncpy(pub_a, p, 64); pub_a[64]='\0'; } check_condition("single-instance public key request", pub_a[0] != '\0'); - snprintf(req, sizeof(req), "{\"id\":\"5\",\"method\":\"nip04_encrypt\",\"params\":[\"%s\",\"hello_nip04\"]}", pub_a); + snprintf(req, sizeof(req), "{\"id\":\"5\",\"method\":\"nostr_nip04_encrypt\",\"params\":[\"%s\",\"hello_nip04\"]}", pub_a); free(resp_a); resp_a = NULL; if (request_roundtrip_to(SOCKET_NAME_A, req, &resp_a) == 0) { p = strstr(resp_a, "\"result\":\""); @@ -1062,7 +1063,7 @@ int main(void) { while (p[i] && p[i] != '\"' && i < sizeof(cipher)-1) { cipher[i]=p[i]; i++; } cipher[i]='\0'; } - snprintf(req, sizeof(req), "{\"id\":\"6\",\"method\":\"nip04_decrypt\",\"params\":[\"%s\",\"%s\"]}", pub_a, cipher); + snprintf(req, sizeof(req), "{\"id\":\"6\",\"method\":\"nostr_nip04_decrypt\",\"params\":[\"%s\",\"%s\"]}", pub_a, cipher); free(resp_a); resp_a = NULL; if (request_roundtrip_to(SOCKET_NAME_A, req, &resp_a) == 0) { check_condition("nip04 round-trip plaintext recovered", strstr(resp_a, "hello_nip04") != NULL); @@ -1074,7 +1075,7 @@ int main(void) { } memset(cipher, 0, sizeof(cipher)); - snprintf(req, sizeof(req), "{\"id\":\"7\",\"method\":\"nip44_encrypt\",\"params\":[\"%s\",\"hello_nip44\"]}", pub_a); + snprintf(req, sizeof(req), "{\"id\":\"7\",\"method\":\"nostr_nip44_encrypt\",\"params\":[\"%s\",\"hello_nip44\"]}", pub_a); free(resp_a); resp_a = NULL; if (request_roundtrip_to(SOCKET_NAME_A, req, &resp_a) == 0) { p = strstr(resp_a, "\"result\":\""); @@ -1084,7 +1085,7 @@ int main(void) { while (p[i] && p[i] != '\"' && i < sizeof(cipher)-1) { cipher[i]=p[i]; i++; } cipher[i]='\0'; } - snprintf(req, sizeof(req), "{\"id\":\"8\",\"method\":\"nip44_decrypt\",\"params\":[\"%s\",\"%s\"]}", pub_a, cipher); + snprintf(req, sizeof(req), "{\"id\":\"8\",\"method\":\"nostr_nip44_decrypt\",\"params\":[\"%s\",\"%s\"]}", pub_a, cipher); free(resp_a); resp_a = NULL; if (request_roundtrip_to(SOCKET_NAME_A, req, &resp_a) == 0) { check_condition("nip44 round-trip plaintext recovered", strstr(resp_a, "hello_nip44") != NULL); diff --git a/tests/test_mine_event.c b/tests/test_mine_event.c index edca491..703c84a 100644 --- a/tests/test_mine_event.c +++ b/tests/test_mine_event.c @@ -1,4 +1,4 @@ -/* Test for mine_event verb (NIP-13 Proof-of-Work) */ +/* Test for nostr_mine_event verb (NIP-13 Proof-of-Work) */ /* NSIGNER_HEADERLESS_DECLS_BEGIN */ #include <stddef.h> #include <stdint.h> @@ -126,18 +126,14 @@ const char *selector_strerror(int err); #define ENFORCE_ERR_CURVE -2 #define ENFORCE_ERR_UNKNOWN_VERB -3 -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" -#define VERB_MINE_EVENT "mine_event" -#define VERB_SIGN_DATA "sign_data" -#define VERB_VERIFY_SIG "verify_signature" -#define VERB_SSH_SIGN "ssh_sign" -#define VERB_KEM_ENCAPS "kem_encapsulate" -#define VERB_KEM_DECAPS "kem_decapsulate" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_MINE_EVENT "nostr_mine_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" int enforce_verb_role(const char *verb, const role_entry_t *role); const char *enforce_strerror(int err); @@ -497,7 +493,7 @@ static int response_has(const char *response, const char *needle) { return (response != NULL && needle != NULL && strstr(response, needle) != NULL); } -/* Parse a dispatcher response and extract the result object (for mine_event). +/* Parse a dispatcher response and extract the result object (for nostr_mine_event). * Returns a newly-allocated cJSON result object, or NULL on failure. * Caller must delete the returned object. */ static cJSON *extract_result_object(const char *response) { @@ -580,67 +576,67 @@ int main(void) { derived = crypto_derive_all(&key_store, &table, &mnemonic); check_condition("crypto_derive_all derives at least one key", derived >= 1); - /* 1. mine_event with low difficulty (2) and short timeout (5 sec) — should reach target */ + /* 1. nostr_mine_event with low difficulty (2) and short timeout (5 sec) — should reach target */ resp = dispatcher_handle_request(&dispatcher, - "{\"id\":\"1\",\"method\":\"mine_event\",\"params\":[" + "{\"id\":\"1\",\"method\":\"nostr_mine_event\",\"params\":[" "\"{\\\"kind\\\":1,\\\"content\\\":\\\"hello pow\\\",\\\"tags\\\":[]}\"," "{\"difficulty\":2,\"threads\":2,\"timeout_sec\":5}]}"); - check_condition("mine_event low difficulty returns result object", + check_condition("nostr_mine_event low difficulty returns result object", response_has(resp, "\"id\":\"1\"") && response_has(resp, "\"result\"") && response_has(resp, "achieved_difficulty")); - check_condition("mine_event low difficulty returns signed event with nonce tag", + check_condition("nostr_mine_event low difficulty returns signed event with nonce tag", response_has(resp, "nonce") && response_has(resp, "sig") && response_has(resp, "pubkey")); - check_condition("mine_event low difficulty target_reached is true", + check_condition("nostr_mine_event low difficulty target_reached is true", check_target_reached(resp, 1)); free(resp); - /* 2. mine_event with high difficulty (30) and short timeout (2 sec) — should NOT reach target */ + /* 2. nostr_mine_event with high difficulty (30) and short timeout (2 sec) — should NOT reach target */ resp = dispatcher_handle_request(&dispatcher, - "{\"id\":\"2\",\"method\":\"mine_event\",\"params\":[" + "{\"id\":\"2\",\"method\":\"nostr_mine_event\",\"params\":[" "\"{\\\"kind\\\":1,\\\"content\\\":\\\"hard pow\\\",\\\"tags\\\":[]}\"," "{\"difficulty\":30,\"threads\":4,\"timeout_sec\":2}]}"); - check_condition("mine_event high difficulty returns result object", + check_condition("nostr_mine_event high difficulty returns result object", response_has(resp, "\"id\":\"2\"") && response_has(resp, "\"result\"") && response_has(resp, "achieved_difficulty")); - check_condition("mine_event high difficulty target_reached is false", + check_condition("nostr_mine_event high difficulty target_reached is false", check_target_reached(resp, 0)); - check_condition("mine_event high difficulty still returns a signed event", + check_condition("nostr_mine_event high difficulty still returns a signed event", response_has(resp, "nonce") && response_has(resp, "sig")); free(resp); - /* 3. mine_event with only timeout (no difficulty) — should mine for full duration */ + /* 3. nostr_mine_event with only timeout (no difficulty) — should mine for full duration */ resp = dispatcher_handle_request(&dispatcher, - "{\"id\":\"3\",\"method\":\"mine_event\",\"params\":[" + "{\"id\":\"3\",\"method\":\"nostr_mine_event\",\"params\":[" "\"{\\\"kind\\\":1,\\\"content\\\":\\\"timeout only\\\",\\\"tags\\\":[]}\"," "{\"threads\":2,\"timeout_sec\":2}]}"); - check_condition("mine_event timeout-only returns result", + check_condition("nostr_mine_event timeout-only returns result", response_has(resp, "\"id\":\"3\"") && response_has(resp, "\"result\"") && response_has(resp, "achieved_difficulty")); - check_condition("mine_event timeout-only target_reached is false (no target set)", + check_condition("nostr_mine_event timeout-only target_reached is false (no target set)", check_target_reached(resp, 0)); free(resp); - /* 4. mine_event with neither difficulty nor timeout — should error */ + /* 4. nostr_mine_event with neither difficulty nor timeout — should error */ resp = dispatcher_handle_request(&dispatcher, - "{\"id\":\"4\",\"method\":\"mine_event\",\"params\":[" + "{\"id\":\"4\",\"method\":\"nostr_mine_event\",\"params\":[" "\"{\\\"kind\\\":1,\\\"content\\\":\\\"no params\\\",\\\"tags\\\":[]}\"," "{\"threads\":2}]}"); - check_condition("mine_event with no termination condition returns 1007", + check_condition("nostr_mine_event with no termination condition returns 1007", response_has(resp, "\"id\":\"4\"") && response_has(resp, "\"code\":1007") && response_has(resp, "no_termination_condition")); free(resp); - /* 5. mine_event with only difficulty (no timeout) — should use safety timeout and reach low target */ + /* 5. nostr_mine_event with only difficulty (no timeout) — should use safety timeout and reach low target */ resp = dispatcher_handle_request(&dispatcher, - "{\"id\":\"5\",\"method\":\"mine_event\",\"params\":[" + "{\"id\":\"5\",\"method\":\"nostr_mine_event\",\"params\":[" "\"{\\\"kind\\\":1,\\\"content\\\":\\\"difficulty only\\\",\\\"tags\\\":[]}\"," "{\"difficulty\":1,\"threads\":2}]}"); - check_condition("mine_event difficulty-only returns result and reaches target", + check_condition("nostr_mine_event difficulty-only returns result and reaches target", response_has(resp, "\"id\":\"5\"") && response_has(resp, "\"result\"") && check_target_reached(resp, 1)); free(resp); - /* 6. mine_event with invalid event JSON */ + /* 6. nostr_mine_event with invalid event JSON */ resp = dispatcher_handle_request(&dispatcher, - "{\"id\":\"6\",\"method\":\"mine_event\",\"params\":[" + "{\"id\":\"6\",\"method\":\"nostr_mine_event\",\"params\":[" "\"not valid json\"," "{\"difficulty\":1,\"timeout_sec\":2}]}"); - check_condition("mine_event with invalid event returns error", + check_condition("nostr_mine_event with invalid event returns error", response_has(resp, "\"id\":\"6\"") && (response_has(resp, "\"code\":1008") || response_has(resp, "\"code\":-32602"))); free(resp); @@ -651,7 +647,7 @@ int main(void) { int pow_diff; resp = dispatcher_handle_request(&dispatcher, - "{\"id\":\"7\",\"method\":\"mine_event\",\"params\":[" + "{\"id\":\"7\",\"method\":\"nostr_mine_event\",\"params\":[" "\"{\\\"kind\\\":1,\\\"content\\\":\\\"verify pow\\\",\\\"tags\\\":[]}\"," "{\"difficulty\":4,\"threads\":4,\"timeout_sec\":5}]}"); diff --git a/tests/test_ml_dsa_65.c b/tests/test_ml_dsa_65.c index e3a6126..7835229 100644 --- a/tests/test_ml_dsa_65.c +++ b/tests/test_ml_dsa_65.c @@ -8,8 +8,8 @@ * - ML-DSA-65 sign/verify with wrong key: verify fails * - DRBG determinism: same seed -> same randombytes output * - Integration: derive ML-DSA-65 key from mnemonic via crypto_derive_one, - * sign data via sign_data verb through dispatcher, verify signature - * - Enforcement: sign_data allowed on pq-sig+ml-dsa-65 + * sign data via sign verb through dispatcher, verify signature + * - Enforcement: sign allowed on pq-sig+ml-dsa-65 */ /* NSIGNER_HEADERLESS_DECLS_BEGIN */ #include <stddef.h> @@ -142,19 +142,21 @@ const char *selector_strerror(int err); #define ENFORCE_ERR_PURPOSE -1 #define ENFORCE_ERR_CURVE -2 #define ENFORCE_ERR_UNKNOWN_VERB -3 +#define ENFORCE_ERR_ALGORITHM -4 -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" -#define VERB_MINE_EVENT "mine_event" -#define VERB_SIGN_DATA "sign_data" -#define VERB_VERIFY_SIG "verify_signature" -#define VERB_SSH_SIGN "ssh_sign" -#define VERB_KEM_ENCAPS "kem_encapsulate" -#define VERB_KEM_DECAPS "kem_decapsulate" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_SIGN "sign" +#define VERB_VERIFY "verify" +#define VERB_ENCAPSULATE "encapsulate" +#define VERB_DECAPSULATE "decapsulate" +#define VERB_DERIVE_SHARED "derive_shared_secret" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_MINE_EVENT "nostr_mine_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" int enforce_verb_role(const char *verb, const role_entry_t *role); const char *enforce_strerror(int err); @@ -682,7 +684,7 @@ int main(void) { mnemonic_unload(&mnemonic_state); } - /* ---- Integration: sign_data via dispatcher ---- */ + /* ---- Integration: sign via dispatcher ---- */ { role_table_t table; role_entry_t pq_role; @@ -705,16 +707,16 @@ int main(void) { dispatcher_init(&dispatcher, &table, &mnemonic_state, &g_key_store, &g_alg_key_cache); - /* sign_data request */ + /* sign request */ snprintf(request, sizeof(request), - "{\"id\":\"test1\",\"method\":\"sign_data\",\"params\":[\"%s\",{\"role\":\"pq_main\"}]}", + "{\"id\":\"test1\",\"method\":\"sign\",\"params\":[\"%s\",{\"algorithm\":\"ml-dsa-65\",\"index\":0}]}", msg_hex); resp = dispatcher_handle_request(&dispatcher, request); - check_condition("sign_data via dispatcher returns result", + check_condition("sign via dispatcher returns result", resp != NULL && response_has(resp, "\"result\"")); - check_condition("sign_data result contains signature", + check_condition("sign result contains signature", resp != NULL && response_has(resp, "signature")); - check_condition("sign_data result contains algorithm ml-dsa-65", + check_condition("sign result contains algorithm ml-dsa-65", resp != NULL && response_has(resp, "ml-dsa-65")); free(resp); @@ -722,7 +724,7 @@ int main(void) { mnemonic_unload(&mnemonic_state); } - /* ---- Integration: verify_signature roundtrip via dispatcher ---- */ + /* ---- Integration: verify roundtrip via dispatcher ---- */ { role_table_t table; role_entry_t pq_role; @@ -749,10 +751,10 @@ int main(void) { /* Sign */ snprintf(sign_req, sizeof(sign_req), - "{\"id\":\"s1\",\"method\":\"sign_data\",\"params\":[\"%s\",{\"role\":\"pq_main\"}]}", + "{\"id\":\"s1\",\"method\":\"sign\",\"params\":[\"%s\",{\"algorithm\":\"ml-dsa-65\",\"index\":0}]}", msg_hex); sign_resp = dispatcher_handle_request(&dispatcher, sign_req); - check_condition("verify roundtrip: sign_data succeeds", sign_resp != NULL); + check_condition("verify roundtrip: sign succeeds", sign_resp != NULL); /* Extract signature from result */ if (sign_resp != NULL) { @@ -777,28 +779,28 @@ int main(void) { /* Verify */ if (sig_str != NULL) { snprintf(verify_req, sizeof(verify_req), - "{\"id\":\"v1\",\"method\":\"verify_signature\",\"params\":[\"%s\",\"%s\",{\"role\":\"pq_main\"}]}", + "{\"id\":\"v1\",\"method\":\"verify\",\"params\":[\"%s\",\"%s\",{\"algorithm\":\"ml-dsa-65\",\"index\":0}]}", msg_hex, sig_str); verify_resp = dispatcher_handle_request(&dispatcher, verify_req); - check_condition("verify_signature returns valid:true", + check_condition("verify returns valid:true", verify_resp != NULL && response_has(verify_resp, "valid") && response_has(verify_resp, "true")); free(verify_resp); /* Verify with wrong message */ snprintf(verify_req, sizeof(verify_req), - "{\"id\":\"v2\",\"method\":\"verify_signature\",\"params\":[\"00ff\",\"%s\",{\"role\":\"pq_main\"}]}", + "{\"id\":\"v2\",\"method\":\"verify\",\"params\":[\"00ff\",\"%s\",{\"algorithm\":\"ml-dsa-65\",\"index\":0}]}", sig_str); verify_resp = dispatcher_handle_request(&dispatcher, verify_req); - check_condition("verify_signature wrong msg returns valid:false", + check_condition("verify wrong msg returns valid:false", verify_resp != NULL && response_has(verify_resp, "valid") && response_has(verify_resp, "false")); free(verify_resp); free((void *)sig_str); } else { - check_condition("verify_signature returns valid:true", 0); - check_condition("verify_signature wrong msg returns valid:false", 0); + check_condition("verify returns valid:true", 0); + check_condition("verify wrong msg returns valid:false", 0); } free(sign_resp); @@ -806,22 +808,26 @@ int main(void) { mnemonic_unload(&mnemonic_state); } - /* ---- Enforcement: sign_data on pq-sig+ml-dsa-65 ---- */ + /* ---- Enforcement: sign on pq-sig+ml-dsa-65 ---- */ { role_entry_t pq_sig = make_pq_sig_ml_dsa_65_entry("pq", 0); role_entry_t nostr_secp = make_nostr_secp_entry("nostr", 0); - check_condition("enforce sign_data + pq-sig/ml-dsa-65 -> OK", - enforce_verb_role(VERB_SIGN_DATA, &pq_sig) == ENFORCE_OK); + /* sign/verify are algorithm-based now. */ + check_condition("enforce sign + ml-dsa-65 -> OK", + enforce_verb_algorithm(VERB_SIGN, CRYPTO_ALG_ML_DSA_65) == ENFORCE_OK); - check_condition("enforce sign_data + nostr/secp256k1 -> PURPOSE err", - enforce_verb_role(VERB_SIGN_DATA, &nostr_secp) == ENFORCE_ERR_PURPOSE); + check_condition("enforce sign + secp256k1 -> OK", + enforce_verb_algorithm(VERB_SIGN, CRYPTO_ALG_SECP256K1) == ENFORCE_OK); - check_condition("enforce verify_signature + pq-sig/ml-dsa-65 -> OK", - enforce_verb_role(VERB_VERIFY_SIG, &pq_sig) == ENFORCE_OK); + check_condition("enforce verify + ml-dsa-65 -> OK", + enforce_verb_algorithm(VERB_VERIFY, CRYPTO_ALG_ML_DSA_65) == ENFORCE_OK); - check_condition("enforce sign_event + pq-sig/ml-dsa-65 -> PURPOSE err", - enforce_verb_role(VERB_SIGN_EVENT, &pq_sig) == ENFORCE_ERR_PURPOSE); + check_condition("enforce sign + ml-kem-768 -> ALGORITHM err", + enforce_verb_algorithm(VERB_SIGN, CRYPTO_ALG_ML_KEM_768) == ENFORCE_ERR_ALGORITHM); + + check_condition("enforce nostr_sign_event + pq-sig/ml-dsa-65 -> PURPOSE err", + enforce_verb_role(VERB_NOSTR_SIGN_EVENT, &pq_sig) == ENFORCE_ERR_PURPOSE); } /* Cleanup */ diff --git a/tests/test_ml_kem_768.c b/tests/test_ml_kem_768.c index 3d3d37d..c619e40 100644 --- a/tests/test_ml_kem_768.c +++ b/tests/test_ml_kem_768.c @@ -8,7 +8,7 @@ * - Decaps with wrong ciphertext: different shared secret (implicit rejection) * - Integration: derive ML-KEM-768 key from mnemonic, encaps via dispatcher, * decaps via dispatcher, shared secrets match - * - Enforcement: kem_encapsulate/kem_decapsulate allowed on pq-kem+ml-kem-768 + * - Enforcement: encapsulate/decapsulate allowed on pq-kem+ml-kem-768 */ /* NSIGNER_HEADERLESS_DECLS_BEGIN */ #include <stddef.h> @@ -112,19 +112,21 @@ const char *selector_strerror(int err); #define ENFORCE_ERR_PURPOSE -1 #define ENFORCE_ERR_CURVE -2 #define ENFORCE_ERR_UNKNOWN_VERB -3 +#define ENFORCE_ERR_ALGORITHM -4 -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" -#define VERB_MINE_EVENT "mine_event" -#define VERB_SIGN_DATA "sign_data" -#define VERB_VERIFY_SIG "verify_signature" -#define VERB_SSH_SIGN "ssh_sign" -#define VERB_KEM_ENCAPS "kem_encapsulate" -#define VERB_KEM_DECAPS "kem_decapsulate" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_SIGN "sign" +#define VERB_VERIFY "verify" +#define VERB_ENCAPSULATE "encapsulate" +#define VERB_DECAPSULATE "decapsulate" +#define VERB_DERIVE_SHARED "derive_shared_secret" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_MINE_EVENT "nostr_mine_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" int enforce_verb_role(const char *verb, const role_entry_t *role); const char *enforce_strerror(int err); @@ -521,7 +523,7 @@ int main(void) { mnemonic_unload(&mnemonic_state); } - /* ---- Integration: kem_encapsulate / kem_decapsulate via dispatcher ---- */ + /* ---- Integration: encapsulate / decapsulate via dispatcher ---- */ { role_table_t table; role_entry_t pq_role; @@ -552,18 +554,18 @@ int main(void) { check_condition("dispatcher integration: pubkey available", pub_hex != NULL); if (pub_hex != NULL && decaps_req != NULL) { - /* kem_encapsulate request */ + /* encapsulate request */ snprintf(encaps_req, sizeof(encaps_req), - "{\"id\":\"e1\",\"method\":\"kem_encapsulate\",\"params\":[\"%s\",{\"role\":\"pq_kem\"}]}", + "{\"id\":\"e1\",\"method\":\"encapsulate\",\"params\":[\"%s\",{\"algorithm\":\"ml-kem-768\",\"index\":0}]}", pub_hex); encaps_resp = dispatcher_handle_request(&dispatcher, encaps_req); - check_condition("kem_encapsulate via dispatcher returns result", + check_condition("encapsulate via dispatcher returns result", encaps_resp != NULL && response_has(encaps_resp, "\"result\"")); - check_condition("kem_encapsulate result contains ciphertext", + check_condition("encapsulate result contains ciphertext", encaps_resp != NULL && response_has(encaps_resp, "ciphertext")); - check_condition("kem_encapsulate result contains shared_secret", + check_condition("encapsulate result contains shared_secret", encaps_resp != NULL && response_has(encaps_resp, "shared_secret")); - check_condition("kem_encapsulate result contains algorithm ml-kem-768", + check_condition("encapsulate result contains algorithm ml-kem-768", encaps_resp != NULL && response_has(encaps_resp, "ml-kem-768")); /* Extract ciphertext and shared_secret from result */ @@ -584,18 +586,18 @@ int main(void) { cJSON_Delete(encaps_json); } } - check_condition("kem_encapsulate: extracted ciphertext hex", ct_str != NULL); - check_condition("kem_encapsulate: extracted shared_secret hex", ss_encaps_str != NULL); + check_condition("encapsulate: extracted ciphertext hex", ct_str != NULL); + check_condition("encapsulate: extracted shared_secret hex", ss_encaps_str != NULL); - /* kem_decapsulate request */ + /* decapsulate request */ if (ct_str != NULL) { snprintf(decaps_req, 6000, - "{\"id\":\"d1\",\"method\":\"kem_decapsulate\",\"params\":[\"%s\",{\"role\":\"pq_kem\"}]}", + "{\"id\":\"d1\",\"method\":\"decapsulate\",\"params\":[\"%s\",{\"algorithm\":\"ml-kem-768\",\"index\":0}]}", ct_str); decaps_resp = dispatcher_handle_request(&dispatcher, decaps_req); - check_condition("kem_decapsulate via dispatcher returns result", + check_condition("decapsulate via dispatcher returns result", decaps_resp != NULL && response_has(decaps_resp, "\"result\"")); - check_condition("kem_decapsulate result contains shared_secret", + check_condition("decapsulate result contains shared_secret", decaps_resp != NULL && response_has(decaps_resp, "shared_secret")); /* Extract shared_secret from decaps result */ @@ -626,8 +628,8 @@ int main(void) { free((void *)ss_decaps_str); free(decaps_resp); } else { - check_condition("kem_decapsulate via dispatcher returns result", 0); - check_condition("kem_decapsulate result contains shared_secret", 0); + check_condition("decapsulate via dispatcher returns result", 0); + check_condition("decapsulate result contains shared_secret", 0); check_condition("dispatcher encaps/decaps shared secrets match", 0); } @@ -641,22 +643,23 @@ int main(void) { mnemonic_unload(&mnemonic_state); } - /* ---- Enforcement: kem_encapsulate/kem_decapsulate on pq-kem+ml-kem-768 ---- */ + /* ---- Enforcement: encapsulate/decapsulate on pq-kem+ml-kem-768 ---- */ { role_entry_t pq_kem = make_pq_kem_ml_kem_768_entry("pqk", 0); role_entry_t nostr_secp = make_nostr_secp_entry("nostr", 0); - check_condition("enforce kem_encapsulate + pq-kem/ml-kem-768 -> OK", - enforce_verb_role(VERB_KEM_ENCAPS, &pq_kem) == ENFORCE_OK); + /* encapsulate/decapsulate are algorithm-based now. */ + check_condition("enforce encapsulate + ml-kem-768 -> OK", + enforce_verb_algorithm(VERB_ENCAPSULATE, CRYPTO_ALG_ML_KEM_768) == ENFORCE_OK); - check_condition("enforce kem_encapsulate + nostr/secp256k1 -> PURPOSE err", - enforce_verb_role(VERB_KEM_ENCAPS, &nostr_secp) == ENFORCE_ERR_PURPOSE); + check_condition("enforce encapsulate + secp256k1 -> ALGORITHM err", + enforce_verb_algorithm(VERB_ENCAPSULATE, CRYPTO_ALG_SECP256K1) == ENFORCE_ERR_ALGORITHM); - check_condition("enforce kem_decapsulate + pq-kem/ml-kem-768 -> OK", - enforce_verb_role(VERB_KEM_DECAPS, &pq_kem) == ENFORCE_OK); + check_condition("enforce decapsulate + ml-kem-768 -> OK", + enforce_verb_algorithm(VERB_DECAPSULATE, CRYPTO_ALG_ML_KEM_768) == ENFORCE_OK); - check_condition("enforce kem_decapsulate + nostr/secp256k1 -> PURPOSE err", - enforce_verb_role(VERB_KEM_DECAPS, &nostr_secp) == ENFORCE_ERR_PURPOSE); + check_condition("enforce decapsulate + ed25519 -> ALGORITHM err", + enforce_verb_algorithm(VERB_DECAPSULATE, CRYPTO_ALG_ED25519) == ENFORCE_ERR_ALGORITHM); } /* ---- Cleanup ---- */ diff --git a/tests/test_mnemonic.c b/tests/test_mnemonic.c index 4488071..c6a3db6 100644 --- a/tests/test_mnemonic.c +++ b/tests/test_mnemonic.c @@ -220,19 +220,20 @@ const char *selector_strerror(int err); /* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" /* * Check whether `verb` is allowed to execute against `role`. * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. * * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: + * - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require: * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). */ diff --git a/tests/test_policy.c b/tests/test_policy.c index db86ca3..12fc5be 100644 --- a/tests/test_policy.c +++ b/tests/test_policy.c @@ -220,19 +220,20 @@ const char *selector_strerror(int err); /* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" /* * Check whether `verb` is allowed to execute against `role`. * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. * * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: + * - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require: * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). */ @@ -801,10 +802,10 @@ int main(void) { check_condition("parse_preapprove_spec rejects non-numeric nostr_index", rc == -1); policy_init_default(&table, uid); - rc = policy_check(&table, same_uid, "sign_event", "main", "nostr", NULL); + rc = policy_check(&table, same_uid, "nostr_sign_event", "main", "nostr", NULL); check_condition("policy_init_default prompts same uid", rc == POLICY_PROMPT); - rc = policy_check(&table, other_uid, "sign_event", "main", "nostr", NULL); + rc = policy_check(&table, other_uid, "nostr_sign_event", "main", "nostr", NULL); check_condition("policy_init_default prompts different uid", rc == POLICY_PROMPT); /* Insert before catch-all: matching caller allowed, non-matching still prompted */ @@ -820,9 +821,9 @@ int main(void) { rc = policy_table_insert_before_last(&table, &grant); check_condition("policy_table_insert_before_last succeeds", rc == 0); } - rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL); + rc = policy_check(&table, "uid:1000", "nostr_sign_event", "main", "nostr", NULL); check_condition("insert_before_last matching caller returns POLICY_ALLOW", rc == POLICY_ALLOW); - rc = policy_check(&table, "uid:2000", "sign_event", "main", "nostr", NULL); + rc = policy_check(&table, "uid:2000", "nostr_sign_event", "main", "nostr", NULL); check_condition("insert_before_last non-matching caller returns POLICY_PROMPT", rc == POLICY_PROMPT); /* Preapprove parse + insert: matching caller allowed, non-matching prompted */ @@ -831,70 +832,70 @@ int main(void) { check_condition("preapprove parse for policy insert succeeds", rc == 0); rc = policy_table_insert_before_last(&table, &parsed); check_condition("preapprove insert_before_last succeeds", rc == 0); - rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL); + rc = policy_check(&table, "uid:1000", "nostr_sign_event", "main", "nostr", NULL); check_condition("preapprove allows matching caller+role", rc == POLICY_ALLOW); - rc = policy_check(&table, "uid:2000", "sign_event", "main", "nostr", NULL); + rc = policy_check(&table, "uid:2000", "nostr_sign_event", "main", "nostr", NULL); check_condition("preapprove leaves non-matching caller at POLICY_PROMPT", rc == POLICY_PROMPT); /* Exact caller, verb, role, purpose + never => allow */ policy_table_init(&table); - add_single_entry(&table, "uid:1000", "sign_event", "main", "nostr", PROMPT_NEVER); - rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL); + add_single_entry(&table, "uid:1000", "nostr_sign_event", "main", "nostr", PROMPT_NEVER); + rc = policy_check(&table, "uid:1000", "nostr_sign_event", "main", "nostr", NULL); check_condition("exact match returns POLICY_ALLOW", rc == POLICY_ALLOW); /* Wildcard caller + deny => deny */ policy_table_init(&table); - add_single_entry(&table, "*", "sign_event", "main", "nostr", PROMPT_DENY); - rc = policy_check(&table, "uid:2000", "sign_event", "main", "nostr", NULL); + add_single_entry(&table, "*", "nostr_sign_event", "main", "nostr", PROMPT_DENY); + rc = policy_check(&table, "uid:2000", "nostr_sign_event", "main", "nostr", NULL); check_condition("wildcard caller with deny returns POLICY_DENY", rc == POLICY_DENY); /* Caller no match => POLICY_NO_MATCH */ policy_table_init(&table); - add_single_entry(&table, "uid:1000", "sign_event", "main", "nostr", PROMPT_NEVER); - rc = policy_check(&table, "uid:9999", "sign_event", "main", "nostr", NULL); + add_single_entry(&table, "uid:1000", "nostr_sign_event", "main", "nostr", PROMPT_NEVER); + rc = policy_check(&table, "uid:9999", "nostr_sign_event", "main", "nostr", NULL); check_condition("caller mismatch returns POLICY_NO_MATCH", rc == POLICY_NO_MATCH); /* Verb not in list => no match */ policy_table_init(&table); add_single_entry(&table, "uid:1000", "get_public_key", "main", "nostr", PROMPT_NEVER); - rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL); + rc = policy_check(&table, "uid:1000", "nostr_sign_event", "main", "nostr", NULL); check_condition("verb mismatch returns POLICY_NO_MATCH", rc == POLICY_NO_MATCH); /* Role not in list => no match */ policy_table_init(&table); - add_single_entry(&table, "uid:1000", "sign_event", "throwaway", "nostr", PROMPT_NEVER); - rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL); + add_single_entry(&table, "uid:1000", "nostr_sign_event", "throwaway", "nostr", PROMPT_NEVER); + rc = policy_check(&table, "uid:1000", "nostr_sign_event", "main", "nostr", NULL); check_condition("role mismatch returns POLICY_NO_MATCH", rc == POLICY_NO_MATCH); /* Purpose not in list => no match */ policy_table_init(&table); - add_single_entry(&table, "uid:1000", "sign_event", "main", "bitcoin", PROMPT_NEVER); - rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL); + add_single_entry(&table, "uid:1000", "nostr_sign_event", "main", "bitcoin", PROMPT_NEVER); + rc = policy_check(&table, "uid:1000", "nostr_sign_event", "main", "nostr", NULL); check_condition("purpose mismatch returns POLICY_NO_MATCH", rc == POLICY_NO_MATCH); /* Empty verbs list means all verbs */ policy_table_init(&table); add_single_entry(&table, "uid:1000", NULL, "main", "nostr", PROMPT_NEVER); - rc = policy_check(&table, "uid:1000", "nip44_encrypt", "main", "nostr", NULL); + rc = policy_check(&table, "uid:1000", "nostr_nip44_encrypt", "main", "nostr", NULL); check_condition("empty verbs list matches any verb", rc == POLICY_ALLOW); /* prompt=first_per_boot => POLICY_PROMPT */ policy_table_init(&table); - add_single_entry(&table, "uid:1000", "sign_event", "main", "nostr", PROMPT_FIRST_PER_BOOT); - rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL); + add_single_entry(&table, "uid:1000", "nostr_sign_event", "main", "nostr", PROMPT_FIRST_PER_BOOT); + rc = policy_check(&table, "uid:1000", "nostr_sign_event", "main", "nostr", NULL); check_condition("first_per_boot returns POLICY_PROMPT", rc == POLICY_PROMPT); /* prompt=every_request => POLICY_PROMPT */ policy_table_init(&table); - add_single_entry(&table, "uid:1000", "sign_event", "main", "nostr", PROMPT_EVERY_REQUEST); - rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL); + add_single_entry(&table, "uid:1000", "nostr_sign_event", "main", "nostr", PROMPT_EVERY_REQUEST); + rc = policy_check(&table, "uid:1000", "nostr_sign_event", "main", "nostr", NULL); check_condition("every_request returns POLICY_PROMPT", rc == POLICY_PROMPT); /* First matching entry wins */ policy_table_init(&table); - add_single_entry(&table, "uid:1000", "sign_event", "main", "nostr", PROMPT_DENY); - add_single_entry(&table, "uid:1000", "sign_event", "main", "nostr", PROMPT_NEVER); - rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL); + add_single_entry(&table, "uid:1000", "nostr_sign_event", "main", "nostr", PROMPT_DENY); + add_single_entry(&table, "uid:1000", "nostr_sign_event", "main", "nostr", PROMPT_NEVER); + rc = policy_check(&table, "uid:1000", "nostr_sign_event", "main", "nostr", NULL); check_condition("first matching entry wins", rc == POLICY_DENY); /* Verify out_source for preapprove, session-grant, and default catch-all */ @@ -931,15 +932,15 @@ int main(void) { check_condition("source test default catch-all add", rc == 0); src = POLICY_SOURCE_DEFAULT; - rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", &src); + rc = policy_check(&table, "uid:1000", "nostr_sign_event", "main", "nostr", &src); check_condition("policy_check reports preapprove source", rc == POLICY_ALLOW && src == POLICY_SOURCE_PREAPPROVE); src = POLICY_SOURCE_DEFAULT; - rc = policy_check(&table, "uid:1001", "sign_event", "main", "nostr", &src); + rc = policy_check(&table, "uid:1001", "nostr_sign_event", "main", "nostr", &src); check_condition("policy_check reports session-grant source", rc == POLICY_ALLOW && src == POLICY_SOURCE_SESSION_GRANT); src = POLICY_SOURCE_PREAPPROVE; - rc = policy_check(&table, "uid:9999", "sign_event", "main", "nostr", &src); + rc = policy_check(&table, "uid:9999", "nostr_sign_event", "main", "nostr", &src); check_condition("policy_check resets source for prompt/default", rc == POLICY_PROMPT && src == POLICY_SOURCE_DEFAULT); } diff --git a/tests/test_pq_crypto.c b/tests/test_pq_crypto.c index e6762b1..6764801 100644 --- a/tests/test_pq_crypto.c +++ b/tests/test_pq_crypto.c @@ -148,12 +148,13 @@ const char *selector_strerror(int err); #define ENFORCE_ERR_CURVE -2 #define ENFORCE_ERR_UNKNOWN_VERB -3 -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" int enforce_verb_role(const char *verb, const role_entry_t *role); const char *enforce_strerror(int err); diff --git a/tests/test_pubkey_format.c b/tests/test_pubkey_format.c index 6a5da4f..1e61b0e 100644 --- a/tests/test_pubkey_format.c +++ b/tests/test_pubkey_format.c @@ -145,18 +145,14 @@ const char *selector_strerror(int err); #define ENFORCE_ERR_CURVE -2 #define ENFORCE_ERR_UNKNOWN_VERB -3 -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" -#define VERB_MINE_EVENT "mine_event" -#define VERB_SIGN_DATA "sign_data" -#define VERB_VERIFY_SIG "verify_signature" -#define VERB_SSH_SIGN "ssh_sign" -#define VERB_KEM_ENCAPS "kem_encapsulate" -#define VERB_KEM_DECAPS "kem_decapsulate" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_MINE_EVENT "nostr_mine_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" int enforce_verb_role(const char *verb, const role_entry_t *role); const char *enforce_strerror(int err); @@ -539,16 +535,23 @@ int main(void) { dispatcher_init(&dispatcher, &table, &mnemonic_state, &g_key_store, &g_alg_key_cache); resp = dispatcher_handle_request(&dispatcher, - "{\"id\":\"1\",\"method\":\"get_public_key\",\"params\":[{\"role\":\"main\"}]}"); + "{\"id\":\"1\",\"method\":\"get_public_key\",\"params\":[{\"algorithm\":\"secp256k1\",\"index\":0}]}"); check_condition("secp256k1 get_public_key returns a response", resp != NULL); + /* Algorithm-based get_public_key always returns a structured object. */ pr = parse_result(resp, &result_str, &result_obj); - check_condition("secp256k1 get_public_key result is a plain string (not object)", - pr == 1 && result_str != NULL); - check_condition("secp256k1 plain hex result is 64 hex chars", - result_str != NULL && strlen(result_str) == 64); - check_condition("secp256k1 plain hex result has no 'algorithm' field", - !response_has(resp, "algorithm")); + check_condition("secp256k1 get_public_key result is a JSON object", + pr == 2 && result_obj != NULL); + if (result_obj != NULL) { + cJSON *pk_item = cJSON_GetObjectItemCaseSensitive(result_obj, "public_key"); + check_condition("secp256k1 structured has public_key (64 hex chars)", + pk_item != NULL && cJSON_IsString(pk_item) && + strlen(pk_item->valuestring) == 64); + } else { + check_condition("secp256k1 structured has public_key (64 hex chars)", 0); + } + check_condition("secp256k1 structured has algorithm field", + response_has(resp, "algorithm")); free(result_str); cJSON_Delete(result_obj); @@ -584,7 +587,7 @@ int main(void) { dispatcher_init(&dispatcher, &table, &mnemonic_state, &g_key_store, &g_alg_key_cache); resp = dispatcher_handle_request(&dispatcher, - "{\"id\":\"2\",\"method\":\"get_public_key\",\"params\":[{\"role\":\"main\",\"format\":\"structured\"}]}"); + "{\"id\":\"2\",\"method\":\"get_public_key\",\"params\":[{\"algorithm\":\"secp256k1\",\"index\":0,\"format\":\"structured\"}]}"); check_condition("secp256k1 structured get_public_key returns a response", resp != NULL); pr = parse_result(resp, &result_str, &result_obj); @@ -645,7 +648,7 @@ int main(void) { dispatcher_init(&dispatcher, &table, &mnemonic_state, &g_key_store, &g_alg_key_cache); resp = dispatcher_handle_request(&dispatcher, - "{\"id\":\"3\",\"method\":\"get_public_key\",\"params\":[{\"role\":\"ssh_main\"}]}"); + "{\"id\":\"3\",\"method\":\"get_public_key\",\"params\":[{\"algorithm\":\"ed25519\",\"index\":0}]}"); check_condition("ed25519 get_public_key returns a response", resp != NULL); pr = parse_result(resp, &result_str, &result_obj); @@ -710,7 +713,7 @@ int main(void) { check_condition("ML-DSA-65 sizes available", sz != NULL); resp = dispatcher_handle_request(&dispatcher, - "{\"id\":\"4\",\"method\":\"get_public_key\",\"params\":[{\"role\":\"pq_sig\"}]}"); + "{\"id\":\"4\",\"method\":\"get_public_key\",\"params\":[{\"algorithm\":\"ml-dsa-65\",\"index\":0}]}"); check_condition("ML-DSA-65 get_public_key returns a response", resp != NULL); pr = parse_result(resp, &result_str, &result_obj); @@ -775,7 +778,7 @@ int main(void) { check_condition("ML-KEM-768 sizes available", sz != NULL); resp = dispatcher_handle_request(&dispatcher, - "{\"id\":\"5\",\"method\":\"get_public_key\",\"params\":[{\"role\":\"kem_main\"}]}"); + "{\"id\":\"5\",\"method\":\"get_public_key\",\"params\":[{\"algorithm\":\"ml-kem-768\",\"index\":0}]}"); check_condition("ML-KEM-768 get_public_key returns a response", resp != NULL); pr = parse_result(resp, &result_str, &result_obj); diff --git a/tests/test_qrexec_auth.c b/tests/test_qrexec_auth.c index 14897a8..836f6ba 100644 --- a/tests/test_qrexec_auth.c +++ b/tests/test_qrexec_auth.c @@ -304,7 +304,7 @@ int main(void) { check_condition("nostr_init for auth signing", nostr_init() == 0); /* no auth => legacy qubes:personal preapprove should match */ - if (run_qrexec_once("{\"id\":\"1\",\"method\":\"get_public_key\",\"params\":[\"\"]}", &resp) == 0) { + if (run_qrexec_once("{\"id\":\"1\",\"method\":\"nostr_get_public_key\",\"params\":[\"\"]}", &resp) == 0) { check_condition("qrexec optional/no-auth still allows legacy qubes caller preapprove", strstr(resp, "\"result\":") != NULL); free(resp); @@ -313,7 +313,7 @@ int main(void) { check_condition("qrexec optional/no-auth request roundtrip", 0); } - auth_req = build_auth_request(privkey, "2", "get_public_key"); + auth_req = build_auth_request(privkey, "2", "nostr_get_public_key"); check_condition("build qrexec auth request", auth_req != NULL); if (auth_req != NULL) { if (run_qrexec_once(auth_req, &resp) == 0) { @@ -328,7 +328,7 @@ int main(void) { auth_req = NULL; } - auth_req = build_auth_request(privkey, "3", "get_public_key"); + auth_req = build_auth_request(privkey, "3", "nostr_get_public_key"); check_condition("build second qrexec auth request", auth_req != NULL); if (auth_req != NULL) { cJSON *root = cJSON_Parse(auth_req); @@ -338,7 +338,7 @@ int main(void) { if (root != NULL) { method_item = cJSON_GetObjectItemCaseSensitive(root, "method"); if (cJSON_IsString(method_item)) { - cJSON_SetValuestring(method_item, "sign_event"); + cJSON_SetValuestring(method_item, "nostr_sign_event"); } tampered = cJSON_PrintUnformatted(root); cJSON_Delete(root); diff --git a/tests/test_role_table.c b/tests/test_role_table.c index ec6ae2f..894aeb0 100644 --- a/tests/test_role_table.c +++ b/tests/test_role_table.c @@ -223,19 +223,20 @@ const char *selector_strerror(int err); /* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" /* * Check whether `verb` is allowed to execute against `role`. * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. * * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: + * - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require: * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). */ diff --git a/tests/test_selector.c b/tests/test_selector.c index 99f7471..9d8e79e 100644 --- a/tests/test_selector.c +++ b/tests/test_selector.c @@ -220,19 +220,20 @@ const char *selector_strerror(int err); /* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" /* * Check whether `verb` is allowed to execute against `role`. * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. * * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: + * - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require: * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). */ diff --git a/tests/test_slh_dsa_128s.c b/tests/test_slh_dsa_128s.c index 2a389ce..b4f0912 100644 --- a/tests/test_slh_dsa_128s.c +++ b/tests/test_slh_dsa_128s.c @@ -8,8 +8,8 @@ * - SLH-DSA-128s sign/verify with wrong key: verify fails * - DRBG determinism: same seed -> same randombytes output * - Integration: derive SLH-DSA-128s key from mnemonic via crypto_derive_one, - * sign data via sign_data verb through dispatcher, verify signature - * - Enforcement: sign_data allowed on pq-sig+slh-dsa-128s + * sign data via sign verb through dispatcher, verify signature + * - Enforcement: sign allowed on pq-sig+slh-dsa-128s */ /* NSIGNER_HEADERLESS_DECLS_BEGIN */ #include <stddef.h> @@ -142,19 +142,21 @@ const char *selector_strerror(int err); #define ENFORCE_ERR_PURPOSE -1 #define ENFORCE_ERR_CURVE -2 #define ENFORCE_ERR_UNKNOWN_VERB -3 +#define ENFORCE_ERR_ALGORITHM -4 -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" -#define VERB_MINE_EVENT "mine_event" -#define VERB_SIGN_DATA "sign_data" -#define VERB_VERIFY_SIG "verify_signature" -#define VERB_SSH_SIGN "ssh_sign" -#define VERB_KEM_ENCAPS "kem_encapsulate" -#define VERB_KEM_DECAPS "kem_decapsulate" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_SIGN "sign" +#define VERB_VERIFY "verify" +#define VERB_ENCAPSULATE "encapsulate" +#define VERB_DECAPSULATE "decapsulate" +#define VERB_DERIVE_SHARED "derive_shared_secret" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_MINE_EVENT "nostr_mine_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" int enforce_verb_role(const char *verb, const role_entry_t *role); const char *enforce_strerror(int err); @@ -682,7 +684,7 @@ int main(void) { mnemonic_unload(&mnemonic_state); } - /* ---- Integration: sign_data via dispatcher ---- */ + /* ---- Integration: sign via dispatcher ---- */ { role_table_t table; role_entry_t pq_role; @@ -705,16 +707,16 @@ int main(void) { dispatcher_init(&dispatcher, &table, &mnemonic_state, &g_key_store, &g_alg_key_cache); - /* sign_data request */ + /* sign request */ snprintf(request, sizeof(request), - "{\"id\":\"test1\",\"method\":\"sign_data\",\"params\":[\"%s\",{\"role\":\"pq_main\"}]}", + "{\"id\":\"test1\",\"method\":\"sign\",\"params\":[\"%s\",{\"algorithm\":\"slh-dsa-128s\",\"index\":0}]}", msg_hex); resp = dispatcher_handle_request(&dispatcher, request); - check_condition("sign_data via dispatcher returns result", + check_condition("sign via dispatcher returns result", resp != NULL && response_has(resp, "\"result\"")); - check_condition("sign_data result contains signature", + check_condition("sign result contains signature", resp != NULL && response_has(resp, "signature")); - check_condition("sign_data result contains algorithm slh-dsa-128s", + check_condition("sign result contains algorithm slh-dsa-128s", resp != NULL && response_has(resp, "slh-dsa-128s")); free(resp); @@ -722,7 +724,7 @@ int main(void) { mnemonic_unload(&mnemonic_state); } - /* ---- Integration: verify_signature roundtrip via dispatcher ---- */ + /* ---- Integration: verify roundtrip via dispatcher ---- */ { role_table_t table; role_entry_t pq_role; @@ -752,10 +754,10 @@ int main(void) { /* Sign */ snprintf(sign_req, sizeof(sign_req), - "{\"id\":\"s1\",\"method\":\"sign_data\",\"params\":[\"%s\",{\"role\":\"pq_main\"}]}", + "{\"id\":\"s1\",\"method\":\"sign\",\"params\":[\"%s\",{\"algorithm\":\"slh-dsa-128s\",\"index\":0}]}", msg_hex); sign_resp = dispatcher_handle_request(&dispatcher, sign_req); - check_condition("verify roundtrip: sign_data succeeds", sign_resp != NULL); + check_condition("verify roundtrip: sign succeeds", sign_resp != NULL); /* Extract signature from result */ if (sign_resp != NULL) { @@ -780,28 +782,28 @@ int main(void) { /* Verify */ if (sig_str != NULL && verify_req != NULL) { snprintf(verify_req, 20000, - "{\"id\":\"v1\",\"method\":\"verify_signature\",\"params\":[\"%s\",\"%s\",{\"role\":\"pq_main\"}]}", + "{\"id\":\"v1\",\"method\":\"verify\",\"params\":[\"%s\",\"%s\",{\"algorithm\":\"slh-dsa-128s\",\"index\":0}]}", msg_hex, sig_str); verify_resp = dispatcher_handle_request(&dispatcher, verify_req); - check_condition("verify_signature returns valid:true", + check_condition("verify returns valid:true", verify_resp != NULL && response_has(verify_resp, "valid") && response_has(verify_resp, "true")); free(verify_resp); /* Verify with wrong message */ snprintf(verify_req, 20000, - "{\"id\":\"v2\",\"method\":\"verify_signature\",\"params\":[\"00ff\",\"%s\",{\"role\":\"pq_main\"}]}", + "{\"id\":\"v2\",\"method\":\"verify\",\"params\":[\"00ff\",\"%s\",{\"algorithm\":\"slh-dsa-128s\",\"index\":0}]}", sig_str); verify_resp = dispatcher_handle_request(&dispatcher, verify_req); - check_condition("verify_signature wrong msg returns valid:false", + check_condition("verify wrong msg returns valid:false", verify_resp != NULL && response_has(verify_resp, "valid") && response_has(verify_resp, "false")); free(verify_resp); free((void *)sig_str); } else { - check_condition("verify_signature returns valid:true", 0); - check_condition("verify_signature wrong msg returns valid:false", 0); + check_condition("verify returns valid:true", 0); + check_condition("verify wrong msg returns valid:false", 0); } free(verify_req); @@ -810,19 +812,23 @@ int main(void) { mnemonic_unload(&mnemonic_state); } - /* ---- Enforcement: sign_data on pq-sig+slh-dsa-128s ---- */ + /* ---- Enforcement: sign on pq-sig+slh-dsa-128s ---- */ { role_entry_t pq_sig = make_pq_sig_slh_dsa_128s_entry("pq", 0); role_entry_t nostr_secp = make_nostr_secp_entry("nostr", 0); - check_condition("enforce sign_data + pq-sig/slh-dsa-128s -> OK", - enforce_verb_role(VERB_SIGN_DATA, &pq_sig) == ENFORCE_OK); + /* sign/verify are algorithm-based now. */ + check_condition("enforce sign + slh-dsa-128s -> OK", + enforce_verb_algorithm(VERB_SIGN, CRYPTO_ALG_SLH_DSA_128S) == ENFORCE_OK); - check_condition("enforce sign_data + nostr/secp256k1 -> PURPOSE err", - enforce_verb_role(VERB_SIGN_DATA, &nostr_secp) == ENFORCE_ERR_PURPOSE); + check_condition("enforce sign + secp256k1 -> OK", + enforce_verb_algorithm(VERB_SIGN, CRYPTO_ALG_SECP256K1) == ENFORCE_OK); - check_condition("enforce verify_signature + pq-sig/slh-dsa-128s -> OK", - enforce_verb_role(VERB_VERIFY_SIG, &pq_sig) == ENFORCE_OK); + check_condition("enforce verify + slh-dsa-128s -> OK", + enforce_verb_algorithm(VERB_VERIFY, CRYPTO_ALG_SLH_DSA_128S) == ENFORCE_OK); + + check_condition("enforce sign + ml-kem-768 -> ALGORITHM err", + enforce_verb_algorithm(VERB_SIGN, CRYPTO_ALG_ML_KEM_768) == ENFORCE_ERR_ALGORITHM); } /* ---- Cleanup ---- */ diff --git a/tests/test_socket_name.c b/tests/test_socket_name.c index f8feffa..03b60ea 100644 --- a/tests/test_socket_name.c +++ b/tests/test_socket_name.c @@ -220,19 +220,20 @@ const char *selector_strerror(int err); /* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" +#define VERB_GET_PUBLIC_KEY "get_public_key" +#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" +#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" +#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" +#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" +#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" +#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" /* * Check whether `verb` is allowed to execute against `role`. * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. * * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: + * - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require: * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). */