diff --git a/README.md b/README.md index 31641f4..20b76ca 100644 --- a/README.md +++ b/README.md @@ -281,6 +281,7 @@ credentials. | `--nsigner-transport ` | (nsigner) `serial\|unix\|tcp\|qrexec` | | `--nsigner-device ` | (nsigner) device path / socket / host:port / qube | | `--nsigner-service ` | (nsigner) qrexec service name | +| `--nsigner-role ` | (nsigner) role name (default: nostr_range) | | `--nsigner-index ` | (nsigner) key index (default 0) | | `--no-save-identity` | Do not persist identity to disk | | `--login-timeout ` | Override agent login timeout (GTK dialog path) | diff --git a/VERSION b/VERSION index fcae301..9a52cbd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.68 +0.0.69 diff --git a/plans/nsigner-adoption.md b/plans/nsigner-adoption.md new file mode 100644 index 0000000..da0361d --- /dev/null +++ b/plans/nsigner-adoption.md @@ -0,0 +1,283 @@ +# n_signer / nostr_core_lib Adoption Plan + +## Overview + +Adopt the major changes from n_signer (v0.0.57 → v0.1.21) and nostr_core_lib (v0.6.10 → v0.6.13) into sovereign_browser. The key change is migrating from the deprecated `nostr_index` selector to the new `role` + `role_path` selector model. + +## Background + +### n_signer API Changes + +The n_signer project redesigned its API: + +1. **Unified verb naming**: All Nostr-specific verbs now use `nostr_` prefix + - `get_public_key` → `nostr_get_public_key` (for Nostr keys) + - `sign_event` → `nostr_sign_event` + - `nip04_encrypt` → `nostr_nip04_encrypt` + - etc. + +2. **Algorithm-based API**: New generic verbs for non-Nostr crypto + - `get_public_key`, `sign`, `verify`, `encapsulate`, `decapsulate`, `derive_shared_secret`, `derive`, `encrypt`, `decrypt` + - Support for secp256k1, ed25519, x25519, ml-dsa-65, slh-dsa-128s, ml-kem-768, otp + +3. **Role-based selectors**: `nostr_index` is **deprecated** + - Old: `{"nostr_index": N}` → automatically expanded to `m/44'/1237'/N'/0/0` + - New: `{"role": "", "role_path": ""}` — both required together + - The role must be pre-registered in the signer's wizard + - The role_path must match the role's registered template + +4. **New error codes**: + - `2006 nostr_index_deprecated` — `nostr_index` is removed + - `2007 index_deprecated` — `index` removed for nostr verbs + - `2008 role_required` — `role` required when using `role_path` + - `2009 path_required` — `role_path` required for roles with variable templates + +### nostr_core_lib Changes + +The upstream library added: + +1. **New public API** in `nostr_signer.h`: + - `nostr_signer_last_error()` — human-readable error from last failed call + - `nostr_signer_get_info()` — signer metadata (name, version, verbs, algorithms) + - `nostr_signer_get_public_key_alg()` — algorithm-based public key + - `nostr_signer_sign()` / `nostr_signer_verify()` — generic sign/verify + - `nostr_signer_encapsulate()` / `nostr_signer_decapsulate()` — ML-KEM-768 + - `nostr_signer_derive_shared_secret()` — X25519 + - `nostr_signer_otp_encrypt()` / `nostr_signer_otp_decrypt()` — OTP + - `nostr_signer_mine_event()` — NIP-13 proof-of-work + - `nostr_signer_nsigner_from_transport()` / `from_client()` — flexible constructors + - `nostr_signer_nsigner_set_role_path()` — set full BIP-44 path + +2. **Changed behavior**: + - `nostr_signer_nsigner_set_nostr_index()` is now a **compatibility shim** that expands to `role="main"` + `role_path="m/44'/1237'/N'/0/0"` + - Remote backend sends `role` + `role_path` instead of `nostr_index` + - New internal fields: `role_path[128]`, `has_role_path`, `derive_index`, `has_derive_index` + +3. **New files**: `nip034.c/h` (NIP-34 git stuff) + +## Current sovereign_browser Usage + +The browser currently uses these deprecated APIs: + +| File | Line | Current Usage | +|------|------|---------------| +| `src/key_store.c` | 84-86 | `nostr_signer_nsigner_set_nostr_index(signer, identity->nsigner_index)` | +| `src/key_store.h` | 53 | `int nsigner_index;` in `key_store_identity_t` | +| `src/login_dialog.c` | 524 | `nostr_signer_nsigner_set_nostr_index(signer, nostr_index)` | +| `src/login_dialog.c` | 1043-1089 | Key index spinner UI with `m/44'/1237'/N'/0/0` label | +| `src/agent_login.c` | 394 | `nostr_signer_nsigner_set_nostr_index(signer, index)` | +| `src/cli.c` | 63-66 | `--nsigner-index` CLI flag | +| `src/cli.h` | 64 | `int nsigner_index;` in `cli_args_t` | + +## Adoption Plan + +### Phase 1: Update Vendored nostr_core_lib + +Update the vendored library from v0.6.10 to v0.6.13. + +**Tasks:** +1. Sync `nostr_core_lib/` directory with upstream v0.6.13 +2. Verify build succeeds with `make` +3. Test basic login flows still work (local, seed, readonly) + +**New APIs available after update:** +- `nostr_signer_last_error()` — better error messages +- `nostr_signer_get_info()` — signer capability detection +- `nostr_signer_nsigner_set_role_path()` — new selector API +- Algorithm-based verbs (for future use) + +### Phase 2: Migrate to role + role_path Selector + +Replace the deprecated `nostr_index` with explicit `role` + `role_path`. + +#### 2.1 Update Data Structures + +**`src/key_store.h`** — Change `key_store_identity_t`: +```c +/* BEFORE */ +int nsigner_index; /* nostr_index (NIP-06 m/44'/1237'/N'/0/0) */ + +/* AFTER */ +int nsigner_index; /* NIP-06 index (0, 1, 2, ...) */ +char nsigner_role[64]; /* role name (default: "nostr_range") */ +``` + +**`src/cli.h`** — Change `cli_args_t`: +```c +/* BEFORE */ +int nsigner_index; /* --nsigner-index (-1 = unset) */ + +/* AFTER */ +int nsigner_index; /* --nsigner-index (-1 = unset) */ +char *nsigner_role; /* --nsigner-role (default: "nostr_range") */ +``` + +#### 2.2 Update CLI Flags + +**`src/cli.c`**: +- Keep `--nsigner-index ` (unchanged) +- Add `--nsigner-role ` (default: "nostr_range") +- The role + index are combined client-side to form the full role_path +- Update help text + +#### 2.3 Update Login Dialog UI + +**`src/login_dialog.c`** — Add a Role entry field after the Service entry. The Key Index spinner stays the same. + +**BEFORE:** +``` +Connect to n_signer hardware signer: + +Transport: [USB Serial / UNIX Socket / TCP / Other Qube ▼] +Target Qube: [nostr_signer________________________] +Service: [qubes.NsignerRpc________] +Key Index (m/44'/1237'/0'/0/0): [0] + +[n_signer is a foreground, RAM-only hardware signer...] +``` + +**AFTER:** +``` +Connect to n_signer hardware signer: + +Transport: [USB Serial / UNIX Socket / TCP / Other Qube ▼] +Target Qube: [nostr_signer________________________] +Service: [qubes.NsignerRpc________] +Role: [nostr_range_________________] +Key Index (m/44'/1237'/0'/0/0): [0] + +[n_signer is a foreground, RAM-only hardware signer...] +``` + +The Role field is a simple text entry with default value "nostr_range". The Key Index spinner remains unchanged — it still selects the NIP-06 index. The role + index are combined client-side to form the full `role_path` (e.g. role="nostr_range" + index=0 → role_path="m/44'/1237'/0'/0/0"). + +#### 2.4 Update Signer Creation + +**`src/key_store.c`** — `key_store_create_signer()`: +```c +/* BEFORE */ +if (signer && identity->nsigner_index >= 0) { + nostr_signer_nsigner_set_nostr_index(signer, identity->nsigner_index); +} + +/* AFTER */ +if (signer) { + /* Build role_path from role + index: m/44'/1237'/N'/0/0 */ + char role_path[128]; + snprintf(role_path, sizeof(role_path), "m/44'/1237'/%d'/0/0", identity->nsigner_index); + nostr_signer_nsigner_set_role_path(signer, role_path); + /* Note: role is passed to the constructor */ +} +``` + +**`src/login_dialog.c`** — `on_sign_in_clicked()`: +```c +/* BEFORE */ +int rc_idx = nostr_signer_nsigner_set_nostr_index(signer, nostr_index); + +/* AFTER */ +/* Build role_path from role + index */ +char role_path[128]; +snprintf(role_path, sizeof(role_path), "m/44'/1237'/%d'/0/0", nostr_index); +int rc_idx = nostr_signer_nsigner_set_role_path(signer, role_path); +``` + +**`src/agent_login.c`** — `login_nsigner()`: +```c +/* BEFORE */ +int rc = nostr_signer_nsigner_set_nostr_index(signer, index); + +/* AFTER */ +/* Get role from params (default: "nostr_range") */ +const char *role = cJSON_GetStringValue(cJSON_GetObjectItem(params, "role")); +if (!role || !role[0]) role = "nostr_range"; +/* Build role_path from index */ +char role_path[128]; +snprintf(role_path, sizeof(role_path), "m/44'/1237'/%d'/0/0", index); +int rc = nostr_signer_nsigner_set_role_path(signer, role_path); +``` + +#### 2.5 Update MCP/Agent Tools + +**`src/agent_mcp.c`** — Update login tool schema: +```json +{ + "method": "nsigner", + "transport": "qrexec", + "device": "nostr_signer", + "service": "qubes.NsignerRpc", + "role": "nostr_range", + "index": 0 +} +``` + +### Phase 3: Error Handling Improvements + +Use the new `nostr_signer_last_error()` for better error messages. + +**`src/login_dialog.c`** and **`src/agent_login.c`**: +```c +/* BEFORE */ +const char *err_str = nostr_strerror(rc); + +/* AFTER */ +const char *err_str = nostr_signer_last_error(signer); +if (!err_str || err_str[0] == '\0') { + err_str = nostr_strerror(rc); +} +``` + +### Phase 4: Testing + +1. **Unit tests**: Update `tests/test_bookmarks_tree.c` if needed +2. **Integration tests**: + - Test n_signer login with each transport (serial, unix, tcp, qrexec) + - Test with different roles (main, nostr_range, custom) + - Test error cases (unknown role, path mismatch, etc.) +3. **MCP tests**: Test agent login with new role/path parameters + +## Migration Notes + +### Backward Compatibility + +- **Saved identities**: The browser does not persist n_signer identities to disk (in-memory only), so no migration of saved data is needed. +- **CLI scripts**: `--nsigner-index` still works; `--nsigner-role` is optional (defaults to "nostr_range"). +- **MCP/API clients**: The `login` tool's `index` parameter still works; `role` is optional. + +### Default Role + +The default role is **"nostr_range"**: +- This matches the n_signer wizard preset #2 +- The client combines role + index to form the full path: `m/44'/1237'/N'/0/0` +- The n_signer server verifies the path matches the role's registered template + +### How It Works + +1. User selects a role (default: "nostr_range") and key index (default: 0) +2. Client builds the full derivation path: `m/44'/1237'/'/0/0` +3. Client sends `{"role": "", "role_path": ""}` to the signer +4. Signer verifies the path matches the role's template and derives the key + +## Files to Modify + +| File | Changes | +|------|---------| +| `nostr_core_lib/` | Sync to v0.6.13 | +| `src/key_store.h` | Replace `nsigner_index` with `nsigner_role` + `nsigner_role_path` | +| `src/key_store.c` | Update `key_store_create_signer()` to use new API | +| `src/login_dialog.c` | Replace index spinner with role/path UI, update sign-in logic | +| `src/agent_login.c` | Update `login_nsigner()` to accept role/path params | +| `src/cli.c` | Replace `--nsigner-index` with `--nsigner-role` + `--nsigner-role-path` | +| `src/cli.h` | Update `cli_args_t` structure | +| `src/agent_mcp.c` | Update login tool schema and handler | +| `README.md` | Update CLI flags documentation | +| `.roo/agents.md` | Update login tool documentation | + +## Success Criteria + +1. Browser builds successfully with updated nostr_core_lib +2. n_signer login works with all transports (serial, unix, tcp, qrexec) +3. Role + role_path selector works correctly +4. No use of deprecated `nostr_index` API +5. Error messages use `nostr_signer_last_error()` for clarity +6. MCP login tool accepts role/path parameters diff --git a/src/agent_login.c b/src/agent_login.c index de96017..f13a21d 100644 --- a/src/agent_login.c +++ b/src/agent_login.c @@ -330,6 +330,7 @@ static cJSON *login_nsigner(cJSON *params) { const char *transport = cJSON_GetStringValue(cJSON_GetObjectItem(params, "transport")); const char *device = cJSON_GetStringValue(cJSON_GetObjectItem(params, "device")); const char *service = cJSON_GetStringValue(cJSON_GetObjectItem(params, "service")); + const char *role = cJSON_GetStringValue(cJSON_GetObjectItem(params, "role")); cJSON *index_json = cJSON_GetObjectItem(params, "index"); int index = (index_json && cJSON_IsNumber(index_json)) ? index_json->valueint : 0; @@ -340,6 +341,9 @@ static cJSON *login_nsigner(cJSON *params) { return make_error("MISSING_PARAM", "Provide 'device' (path, socket name, host:port, or qube name)"); } + /* Use default role if not provided */ + if (!role || !role[0]) role = "nostr_range"; + /* Block SIGCHLD during n_signer calls (qrexec spawns children). */ sigset_t block_set, old_set; sigemptyset(&block_set); @@ -350,10 +354,10 @@ static cJSON *login_nsigner(cJSON *params) { const char *transport_name = "unknown"; if (strcmp(transport, "serial") == 0) { - signer = nostr_signer_nsigner_serial(device, NULL, 15000); + signer = nostr_signer_nsigner_serial(device, role, 15000); transport_name = "serial"; } else if (strcmp(transport, "unix") == 0) { - signer = nostr_signer_nsigner_unix(device, NULL, 15000); + signer = nostr_signer_nsigner_unix(device, role, 15000); transport_name = "unix"; } else if (strcmp(transport, "tcp") == 0) { char host[256] = {0}; @@ -369,11 +373,11 @@ static cJSON *login_nsigner(cJSON *params) { } else { strncpy(host, device, sizeof(host) - 1); } - signer = nostr_signer_nsigner_tcp(host, port, NULL, 15000); + signer = nostr_signer_nsigner_tcp(host, port, role, 15000); transport_name = "tcp"; } else if (strcmp(transport, "qrexec") == 0) { const char *svc = (service && service[0]) ? service : "qubes.NsignerRpc"; - signer = nostr_signer_nsigner_qrexec(device, svc, NULL, 30000); + signer = nostr_signer_nsigner_qrexec(device, svc, role, 30000); transport_name = "qrexec"; } else { sigprocmask(SIG_SETMASK, &old_set, NULL); @@ -391,17 +395,23 @@ static cJSON *login_nsigner(cJSON *params) { * rejects the call with an auth error that surfaces as "code -310". */ key_store_nsigner_set_default_auth(signer); - int rc = nostr_signer_nsigner_set_nostr_index(signer, index); + /* Build role_path from index: m/44'/1237'/N'/0/0 */ + char role_path[128]; + snprintf(role_path, sizeof(role_path), "m/44'/1237'/%d'/0/0", index); + int rc = nostr_signer_nsigner_set_role_path(signer, role_path); if (rc != NOSTR_SUCCESS) { nostr_signer_free(signer); sigprocmask(SIG_SETMASK, &old_set, NULL); - return make_error("NSIGNER_INDEX", "Failed to set nostr_index on n_signer."); + return make_error("NSIGNER_INDEX", "Failed to set role_path on n_signer."); } char pubkey_hex[65]; rc = nostr_signer_get_public_key(signer, pubkey_hex); if (rc != NOSTR_SUCCESS) { - const char *err_str = nostr_strerror(rc); + const char *err_str = nostr_signer_last_error(signer); + if (!err_str || err_str[0] == '\0' || strcmp(err_str, "no error details available") == 0) { + err_str = nostr_strerror(rc); + } g_printerr("[agent-login] n_signer get_public_key failed: rc=%d (%s)\n", rc, err_str ? err_str : "?"); nostr_signer_free(signer); diff --git a/src/agent_mcp.c b/src/agent_mcp.c index 0b3db52..45009b3 100644 --- a/src/agent_mcp.c +++ b/src/agent_mcp.c @@ -132,7 +132,7 @@ const mcp_tool_def_t tool_defs[] = { {"login", "Log in to the browser with a Nostr identity. Methods: 'random' (generate a fresh random key — quickest for testing), 'local' (nsec or hex privkey), 'seed' (BIP-39 mnemonic), 'readonly' (npub), 'nip46' (bunker:// URL), 'nsigner' (hardware signer). Must be called before browser tools work.", - "{\"type\":\"object\",\"properties\":{\"method\":{\"type\":\"string\",\"enum\":[\"random\",\"local\",\"seed\",\"readonly\",\"nip46\",\"nsigner\"]},\"nsec\":{\"type\":\"string\",\"description\":\"nsec1... string (method: local)\"},\"privkey_hex\":{\"type\":\"string\",\"description\":\"64-char hex private key (method: local)\"},\"mnemonic\":{\"type\":\"string\",\"description\":\"12 or 24 BIP-39 words (method: seed)\"},\"account\":{\"type\":\"integer\",\"default\":0,\"description\":\"Account index (method: seed)\"},\"npub\":{\"type\":\"string\",\"description\":\"npub1... string (method: readonly)\"},\"pubkey_hex\":{\"type\":\"string\",\"description\":\"64-char hex pubkey (method: readonly)\"},\"bunker_url\":{\"type\":\"string\",\"description\":\"bunker:// URL (method: nip46)\"},\"transport\":{\"type\":\"string\",\"enum\":[\"serial\",\"unix\",\"tcp\",\"qrexec\"],\"description\":\"Transport type (method: nsigner)\"},\"device\":{\"type\":\"string\",\"description\":\"Device path, socket, host:port, or qube name (method: nsigner)\"},\"service\":{\"type\":\"string\",\"default\":\"qubes.NsignerRpc\",\"description\":\"Qrexec service name (method: nsigner)\"},\"index\":{\"type\":\"integer\",\"default\":0,\"description\":\"Key index (method: nsigner)\"}},\"required\":[\"method\"]}"}, + "{\"type\":\"object\",\"properties\":{\"method\":{\"type\":\"string\",\"enum\":[\"random\",\"local\",\"seed\",\"readonly\",\"nip46\",\"nsigner\"]},\"nsec\":{\"type\":\"string\",\"description\":\"nsec1... string (method: local)\"},\"privkey_hex\":{\"type\":\"string\",\"description\":\"64-char hex private key (method: local)\"},\"mnemonic\":{\"type\":\"string\",\"description\":\"12 or 24 BIP-39 words (method: seed)\"},\"account\":{\"type\":\"integer\",\"default\":0,\"description\":\"Account index (method: seed)\"},\"npub\":{\"type\":\"string\",\"description\":\"npub1... string (method: readonly)\"},\"pubkey_hex\":{\"type\":\"string\",\"description\":\"64-char hex pubkey (method: readonly)\"},\"bunker_url\":{\"type\":\"string\",\"description\":\"bunker:// URL (method: nip46)\"},\"transport\":{\"type\":\"string\",\"enum\":[\"serial\",\"unix\",\"tcp\",\"qrexec\"],\"description\":\"Transport type (method: nsigner)\"},\"device\":{\"type\":\"string\",\"description\":\"Device path, socket, host:port, or qube name (method: nsigner)\"},\"service\":{\"type\":\"string\",\"default\":\"qubes.NsignerRpc\",\"description\":\"Qrexec service name (method: nsigner)\"},\"role\":{\"type\":\"string\",\"default\":\"nostr_range\",\"description\":\"Role name (method: nsigner)\"},\"index\":{\"type\":\"integer\",\"default\":0,\"description\":\"Key index (method: nsigner)\"}},\"required\":[\"method\"]}"}, {"logout", "Log out and clear the current Nostr identity.", @@ -140,7 +140,7 @@ const mcp_tool_def_t tool_defs[] = { {"switch_identity", "Switch to a new Nostr identity. Same parameters as login (including 'generate'). Frees the old signer first.", - "{\"type\":\"object\",\"properties\":{\"method\":{\"type\":\"string\",\"enum\":[\"generate\",\"local\",\"seed\",\"readonly\",\"nip46\",\"nsigner\"]},\"nsec\":{\"type\":\"string\"},\"privkey_hex\":{\"type\":\"string\"},\"mnemonic\":{\"type\":\"string\"},\"npub\":{\"type\":\"string\"},\"bunker_url\":{\"type\":\"string\"},\"transport\":{\"type\":\"string\"},\"device\":{\"type\":\"string\"},\"index\":{\"type\":\"integer\"}},\"required\":[\"method\"]}"}, + "{\"type\":\"object\",\"properties\":{\"method\":{\"type\":\"string\",\"enum\":[\"generate\",\"local\",\"seed\",\"readonly\",\"nip46\",\"nsigner\"]},\"nsec\":{\"type\":\"string\"},\"privkey_hex\":{\"type\":\"string\"},\"mnemonic\":{\"type\":\"string\"},\"npub\":{\"type\":\"string\"},\"bunker_url\":{\"type\":\"string\"},\"transport\":{\"type\":\"string\"},\"device\":{\"type\":\"string\"},\"role\":{\"type\":\"string\"},\"index\":{\"type\":\"integer\"}},\"required\":[\"method\"]}"}, /* Navigation tools */ {"open", diff --git a/src/cli.c b/src/cli.c index b186842..9e724c6 100644 --- a/src/cli.c +++ b/src/cli.c @@ -63,6 +63,7 @@ void cli_print_usage(FILE *fp) { " --nsigner-transport (nsigner) serial|unix|tcp|qrexec\n" " --nsigner-device (nsigner) device path / socket / host:port / qube\n" " --nsigner-service (nsigner) qrexec service name\n" + " --nsigner-role (nsigner) role name (default: nostr_range)\n" " --nsigner-index (nsigner) key index (default 0)\n" " --login-timeout Override agent login timeout (GTK dialog path)\n" "\n" @@ -103,6 +104,7 @@ enum { OPT_NSIGNER_TRANSPORT, OPT_NSIGNER_DEVICE, OPT_NSIGNER_SERVICE, + OPT_NSIGNER_ROLE, OPT_NSIGNER_INDEX, OPT_NO_LOGIN, OPT_LOGIN_TIMEOUT, @@ -137,6 +139,7 @@ static struct option long_opts[] = { {"nsigner-transport", required_argument, 0, OPT_NSIGNER_TRANSPORT}, {"nsigner-device", required_argument, 0, OPT_NSIGNER_DEVICE}, {"nsigner-service", required_argument, 0, OPT_NSIGNER_SERVICE}, + {"nsigner-role", required_argument, 0, OPT_NSIGNER_ROLE}, {"nsigner-index", required_argument, 0, OPT_NSIGNER_INDEX}, {"no-login", no_argument, 0, OPT_NO_LOGIN}, {"login-timeout", required_argument, 0, OPT_LOGIN_TIMEOUT}, @@ -200,6 +203,7 @@ void cli_args_free(cli_args_t *args) { g_free(args->nsigner_transport); g_free(args->nsigner_device); g_free(args->nsigner_service); + g_free(args->nsigner_role); g_free(args->download_url); memset(args, 0, sizeof(*args)); } @@ -330,6 +334,10 @@ int cli_parse(int *argc, char ***argv, cli_args_t *out) { g_free(out->nsigner_service); out->nsigner_service = xstrdup(optarg); break; + case OPT_NSIGNER_ROLE: + g_free(out->nsigner_role); + out->nsigner_role = xstrdup(optarg); + break; case OPT_NSIGNER_INDEX: out->nsigner_index = atoi(optarg); if (out->nsigner_index < 0) { @@ -441,6 +449,7 @@ int cli_login(const cli_args_t *args) { if (args->nsigner_transport) cJSON_AddStringToObject(params, "transport", args->nsigner_transport); if (args->nsigner_device) cJSON_AddStringToObject(params, "device", args->nsigner_device); if (args->nsigner_service) cJSON_AddStringToObject(params, "service", args->nsigner_service); + if (args->nsigner_role) cJSON_AddStringToObject(params, "role", args->nsigner_role); if (args->nsigner_index >= 0) cJSON_AddNumberToObject(params, "index", args->nsigner_index); /* agent_login() calls app_set_signer() on success, which sets the diff --git a/src/cli.h b/src/cli.h index e46ece7..0976a1e 100644 --- a/src/cli.h +++ b/src/cli.h @@ -61,6 +61,7 @@ typedef struct { char *nsigner_transport; /* --nsigner-transport */ char *nsigner_device; /* --nsigner-device */ char *nsigner_service; /* --nsigner-service */ + char *nsigner_role; /* --nsigner-role (default: "nostr_range") */ int nsigner_index; /* --nsigner-index (-1 = unset) */ int login_timeout_ms; /* --login-timeout (0 = unset) */ diff --git a/src/key_store.c b/src/key_store.c index 5eab591..9153e38 100644 --- a/src/key_store.c +++ b/src/key_store.c @@ -60,10 +60,11 @@ nostr_signer_t *key_store_create_signer(const key_store_identity_t *identity) { case KEY_STORE_METHOD_NSIGNER: { #if defined(NOSTR_ENABLE_NSIGNER_CLIENT) nostr_signer_t *signer = NULL; + const char *role = identity->nsigner_role[0] ? identity->nsigner_role : "nostr_range"; if (strcmp(identity->nsigner_transport, "serial") == 0) { - signer = nostr_signer_nsigner_serial(identity->nsigner_device, NULL, 15000); + signer = nostr_signer_nsigner_serial(identity->nsigner_device, role, 15000); } else if (strcmp(identity->nsigner_transport, "unix") == 0) { - signer = nostr_signer_nsigner_unix(identity->nsigner_device, NULL, 15000); + signer = nostr_signer_nsigner_unix(identity->nsigner_device, role, 15000); } else if (strcmp(identity->nsigner_transport, "tcp") == 0) { /* nsigner_device is "host:port" */ char host[256]; @@ -75,14 +76,17 @@ nostr_signer_t *key_store_create_signer(const key_store_identity_t *identity) { memcpy(host, identity->nsigner_device, hlen); host[hlen] = '\0'; port = atoi(sep + 1); - signer = nostr_signer_nsigner_tcp(host, port, NULL, 15000); + signer = nostr_signer_nsigner_tcp(host, port, role, 15000); } } } else if (strcmp(identity->nsigner_transport, "qrexec") == 0) { - signer = nostr_signer_nsigner_qrexec(identity->nsigner_device, "qubes.NsignerRpc", NULL, 30000); + signer = nostr_signer_nsigner_qrexec(identity->nsigner_device, "qubes.NsignerRpc", role, 30000); } if (signer && identity->nsigner_index >= 0) { - nostr_signer_nsigner_set_nostr_index(signer, identity->nsigner_index); + /* Build role_path from index: m/44'/1237'/N'/0/0 */ + char role_path[128]; + snprintf(role_path, sizeof(role_path), "m/44'/1237'/%d'/0/0", identity->nsigner_index); + nostr_signer_nsigner_set_role_path(signer, role_path); } /* n_signer's serial and TCP transports require a kind-27235 auth * envelope on every request. Install the default caller identity diff --git a/src/key_store.h b/src/key_store.h index 4df8dde..b3aa461 100644 --- a/src/key_store.h +++ b/src/key_store.h @@ -50,7 +50,8 @@ typedef struct { /* n_signer transport (nsigner method only). */ char nsigner_transport[16]; /* "serial" | "unix" | "tcp" | "qrexec" */ char nsigner_device[256]; /* device path / socket name / host:port / qube */ - int nsigner_index; /* nostr_index (NIP-06 m/44'/1237'/N'/0/0) */ + int nsigner_index; /* NIP-06 index (0, 1, 2, ...) */ + char nsigner_role[64]; /* role name (default: "nostr_range") */ } key_store_identity_t; /* diff --git a/src/login_dialog.c b/src/login_dialog.c index 3ceaafb..993a47f 100644 --- a/src/login_dialog.c +++ b/src/login_dialog.c @@ -454,12 +454,17 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) { GtkWidget *screen = get_method_page(ctx, "nsigner"); GtkWidget *transport_combo = g_object_get_data(G_OBJECT(screen), "transport-combo"); GtkWidget *device_entry = g_object_get_data(G_OBJECT(screen), "device-entry"); + GtkWidget *role_entry = g_object_get_data(G_OBJECT(screen), "role-entry"); GtkWidget *index_spin = g_object_get_data(G_OBJECT(screen), "index-spin"); const char *device = gtk_entry_get_text(GTK_ENTRY(device_entry)); + const char *role = gtk_entry_get_text(GTK_ENTRY(role_entry)); int nostr_index = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(index_spin)); gint transport_idx = gtk_combo_box_get_active(GTK_COMBO_BOX(transport_combo)); + /* Use default role if empty */ + if (role[0] == '\0') role = "nostr_range"; + /* Block SIGCHLD during n_signer calls — the qrexec transport spawns * child processes, and SIGCHLD can interrupt gtk_dialog_run's * internal main loop, causing the dialog to close prematurely. */ @@ -479,10 +484,10 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) { const char *transport_name = "serial"; if (transport_idx == 0) { - signer = nostr_signer_nsigner_serial(device, NULL, 15000); + signer = nostr_signer_nsigner_serial(device, role, 15000); transport_name = "serial"; } else if (transport_idx == 1) { - signer = nostr_signer_nsigner_unix(device, NULL, 15000); + signer = nostr_signer_nsigner_unix(device, role, 15000); transport_name = "unix"; } else if (transport_idx == 2) { char host[256]; @@ -494,7 +499,7 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) { memcpy(host, device, hlen); host[hlen] = '\0'; port = atoi(sep + 1); - signer = nostr_signer_nsigner_tcp(host, port, NULL, 15000); + signer = nostr_signer_nsigner_tcp(host, port, role, 15000); transport_name = "tcp"; } } @@ -502,7 +507,7 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) { GtkWidget *service_entry = g_object_get_data(G_OBJECT(screen), "service-entry"); const char *service = service_entry ? gtk_entry_get_text(GTK_ENTRY(service_entry)) : "qubes.NsignerRpc"; if (service[0] == '\0') service = "qubes.NsignerRpc"; - signer = nostr_signer_nsigner_qrexec(device, service, NULL, 30000); + signer = nostr_signer_nsigner_qrexec(device, service, role, 30000); transport_name = "qrexec"; } @@ -521,10 +526,14 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) { * "code -310". */ key_store_nsigner_set_default_auth(signer); - int rc_idx = nostr_signer_nsigner_set_nostr_index(signer, nostr_index); + /* Build role_path from index: m/44'/1237'/N'/0/0 */ + char role_path[128]; + snprintf(role_path, sizeof(role_path), "m/44'/1237'/%d'/0/0", nostr_index); + g_print("[login] n_signer: setting role=%s role_path=%s\n", role, role_path); + int rc_idx = nostr_signer_nsigner_set_role_path(signer, role_path); if (rc_idx != NOSTR_SUCCESS) { gtk_label_set_text(GTK_LABEL(ctx->status_label), - "Failed to set nostr_index on n_signer."); + "Failed to set role_path on n_signer."); nostr_signer_free(signer); sigprocmask(SIG_SETMASK, &old_set, NULL); return; @@ -532,29 +541,26 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) { char pubkey_hex[65]; int rc_pk = nostr_signer_get_public_key(signer, pubkey_hex); + g_print("[login] n_signer: get_public_key returned rc=%d pubkey=%s\n", rc_pk, pubkey_hex); if (rc_pk != NOSTR_SUCCESS) { char errmsg[320]; - const char *desc = "unknown error"; - switch (rc_pk) { - case -5: desc = "I/O failed — signer may have denied the request or disconnected"; break; - case -2001: desc = "policy denied — caller not approved at signer terminal"; break; - case -2002: desc = "index not in signer's whitelist"; break; - case -3: desc = "crypto operation failed"; break; - case NOSTR_ERROR_NIP46_AUTH_CHALLENGE: - /* Device RPC codes 2010-2017: auth-related rejection. - * Most commonly this means the kind-27235 caller auth - * envelope was missing/rejected (the default caller - * identity is installed automatically, but the device's - * policy may still deny it). It can also mean the device - * is requesting on-device approval (button press / TUI - * confirm) for this operation. */ - desc = "auth rejected by n_signer (code -310). The caller " - "auth envelope was missing or denied, or the device " - "is requesting on-device approval. Confirm any " - "prompt on the n_signer hardware, then click Sign " - "In again"; - break; - default: break; + const char *desc = nostr_signer_last_error(signer); + if (!desc || desc[0] == '\0' || strcmp(desc, "no error details available") == 0) { + /* Fall back to error code mapping */ + switch (rc_pk) { + case -5: desc = "I/O failed — signer may have denied the request or disconnected"; break; + case -2001: desc = "policy denied — caller not approved at signer terminal"; break; + case -2002: desc = "index not in signer's whitelist"; break; + case -3: desc = "crypto operation failed"; break; + case NOSTR_ERROR_NIP46_AUTH_CHALLENGE: + desc = "auth rejected by n_signer (code -310). The caller " + "auth envelope was missing or denied, or the device " + "is requesting on-device approval. Confirm any " + "prompt on the n_signer hardware, then click Sign " + "In again"; + break; + default: desc = "unknown error"; break; + } } snprintf(errmsg, sizeof(errmsg), "n_signer error: %s (code %d). Try a different key index.", @@ -570,8 +576,8 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) { char npub[128]; pubkey_to_npub(pubkey_hex, npub); - g_print("[login] n_signer: transport=%s index=%d pubkey=%s npub=%s\n", - transport_name, nostr_index, pubkey_hex, + g_print("[login] n_signer: transport=%s role=%s index=%d pubkey=%s npub=%s\n", + transport_name, role, nostr_index, pubkey_hex, npub[0] ? npub : "(conversion failed)"); ctx->result->method = KEY_STORE_METHOD_NSIGNER; @@ -589,6 +595,9 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) { sizeof(ctx->result->identity.nsigner_device) - 1); ctx->result->identity.nsigner_device[sizeof(ctx->result->identity.nsigner_device) - 1] = '\0'; ctx->result->identity.nsigner_index = nostr_index; + strncpy(ctx->result->identity.nsigner_role, role, + sizeof(ctx->result->identity.nsigner_role) - 1); + ctx->result->identity.nsigner_role[sizeof(ctx->result->identity.nsigner_role) - 1] = '\0'; ctx->done = TRUE; gtk_dialog_response(GTK_DIALOG(ctx->dialog), GTK_RESPONSE_ACCEPT); @@ -1036,6 +1045,19 @@ static GtkWidget *create_nsigner_screen(login_ctx_t *ctx) { /* Show the service field by default (qrexec is the default transport). */ gtk_widget_show_all(service_box); + /* Role entry field. */ + GtkWidget *role_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + gtk_box_pack_start(GTK_BOX(box), role_box, FALSE, FALSE, 0); + + GtkWidget *role_label = gtk_label_new("Role:"); + gtk_box_pack_start(GTK_BOX(role_box), role_label, FALSE, FALSE, 0); + + GtkWidget *role_entry = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(role_entry), "nostr_range"); + gtk_entry_set_placeholder_text(GTK_ENTRY(role_entry), "nostr_range"); + gtk_entry_set_width_chars(GTK_ENTRY(role_entry), 30); + gtk_box_pack_start(GTK_BOX(role_box), role_entry, FALSE, FALSE, 0); + /* Connect transport change callback to update the UI dynamically. */ g_signal_connect(transport_combo, "changed", G_CALLBACK(on_transport_changed), box); @@ -1070,6 +1092,7 @@ static GtkWidget *create_nsigner_screen(login_ctx_t *ctx) { g_object_set_data(G_OBJECT(box), "enum-btn", enum_btn); g_object_set_data(G_OBJECT(box), "service-box", service_box); g_object_set_data(G_OBJECT(box), "service-entry", service_entry); + g_object_set_data(G_OBJECT(box), "role-entry", role_entry); g_object_set_data(G_OBJECT(box), "index-spin", index_spin); g_object_set_data(G_OBJECT(box), "index-label", index_label); return box; diff --git a/src/version.h b/src/version.h index 03aeb6e..1cf6c0c 100644 --- a/src/version.h +++ b/src/version.h @@ -11,9 +11,9 @@ #ifndef SOVEREIGN_BROWSER_VERSION_H #define SOVEREIGN_BROWSER_VERSION_H -#define SB_VERSION "v0.0.68" +#define SB_VERSION "v0.0.69" #define SB_VERSION_MAJOR 0 #define SB_VERSION_MINOR 0 -#define SB_VERSION_PATCH 68 +#define SB_VERSION_PATCH 69 #endif /* SOVEREIGN_BROWSER_VERSION_H */ diff --git a/tests/local-site/shared-worker-absolute-test.html b/tests/local-site/shared-worker-absolute-test.html new file mode 100644 index 0000000..0f816e6 --- /dev/null +++ b/tests/local-site/shared-worker-absolute-test.html @@ -0,0 +1,53 @@ + + + + + +SharedWorker Absolute URL Test + + + +

🔄 SharedWorker Absolute local:// importScripts Test

+

Tests whether importScripts('local:///absolute/path') works inside a SharedWorker.

+ +
Running tests...
+ + + + diff --git a/tests/local-site/shared-worker-absolute-test.js b/tests/local-site/shared-worker-absolute-test.js new file mode 100644 index 0000000..1171513 --- /dev/null +++ b/tests/local-site/shared-worker-absolute-test.js @@ -0,0 +1,21 @@ +/** + * SharedWorker test for importScripts() with absolute local:// URLs. + */ +console.log('[Abs-Import-Test] Worker script executing'); +console.log('[Abs-Import-Test] self.location.href:', self.location.href); + +// Try importScripts with an absolute local:// URL +try { + importScripts('local:///home/user/lt/sovereign_browser/tests/local-site/shared-worker-test.js'); + console.log('[Abs-Import-Test] Absolute local:// importScripts succeeded'); +} catch (e) { + console.log('[Abs-Import-Test] Absolute local:// importScripts FAILED:', e.message); +} + +self.onconnect = function(e) { + var port = e.ports[0]; + port.postMessage('worker_ready'); + port.onmessage = function(e) { + if (e.data === 'ping') port.postMessage('pong'); + }; +};