v0.0.69 - Adopt n_signer role-based API: add --nsigner-role flag, Role entry in login dialog, use nostr_signer_nsigner_set_role_path() and nostr_signer_last_error(), sync nostr_core_lib to v0.6.13

This commit is contained in:
Laan Tungir
2026-08-06 17:03:36 -04:00
parent a710277931
commit 2bd544e54b
13 changed files with 453 additions and 47 deletions
+1
View File
@@ -281,6 +281,7 @@ credentials.
| `--nsigner-transport <t>` | (nsigner) `serial\|unix\|tcp\|qrexec` | | `--nsigner-transport <t>` | (nsigner) `serial\|unix\|tcp\|qrexec` |
| `--nsigner-device <path>` | (nsigner) device path / socket / host:port / qube | | `--nsigner-device <path>` | (nsigner) device path / socket / host:port / qube |
| `--nsigner-service <name>` | (nsigner) qrexec service name | | `--nsigner-service <name>` | (nsigner) qrexec service name |
| `--nsigner-role <name>` | (nsigner) role name (default: nostr_range) |
| `--nsigner-index <n>` | (nsigner) key index (default 0) | | `--nsigner-index <n>` | (nsigner) key index (default 0) |
| `--no-save-identity` | Do not persist identity to disk | | `--no-save-identity` | Do not persist identity to disk |
| `--login-timeout <ms>` | Override agent login timeout (GTK dialog path) | | `--login-timeout <ms>` | Override agent login timeout (GTK dialog path) |
+1 -1
View File
@@ -1 +1 @@
0.0.68 0.0.69
+283
View File
@@ -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": "<name>", "role_path": "<full-bip44-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 <n>` (unchanged)
- Add `--nsigner-role <name>` (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'/<index>'/0/0`
3. Client sends `{"role": "<role>", "role_path": "<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
+17 -7
View File
@@ -330,6 +330,7 @@ static cJSON *login_nsigner(cJSON *params) {
const char *transport = cJSON_GetStringValue(cJSON_GetObjectItem(params, "transport")); const char *transport = cJSON_GetStringValue(cJSON_GetObjectItem(params, "transport"));
const char *device = cJSON_GetStringValue(cJSON_GetObjectItem(params, "device")); const char *device = cJSON_GetStringValue(cJSON_GetObjectItem(params, "device"));
const char *service = cJSON_GetStringValue(cJSON_GetObjectItem(params, "service")); 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"); cJSON *index_json = cJSON_GetObjectItem(params, "index");
int index = (index_json && cJSON_IsNumber(index_json)) ? index_json->valueint : 0; 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)"); 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). */ /* Block SIGCHLD during n_signer calls (qrexec spawns children). */
sigset_t block_set, old_set; sigset_t block_set, old_set;
sigemptyset(&block_set); sigemptyset(&block_set);
@@ -350,10 +354,10 @@ static cJSON *login_nsigner(cJSON *params) {
const char *transport_name = "unknown"; const char *transport_name = "unknown";
if (strcmp(transport, "serial") == 0) { if (strcmp(transport, "serial") == 0) {
signer = nostr_signer_nsigner_serial(device, NULL, 15000); signer = nostr_signer_nsigner_serial(device, role, 15000);
transport_name = "serial"; transport_name = "serial";
} else if (strcmp(transport, "unix") == 0) { } 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"; transport_name = "unix";
} else if (strcmp(transport, "tcp") == 0) { } else if (strcmp(transport, "tcp") == 0) {
char host[256] = {0}; char host[256] = {0};
@@ -369,11 +373,11 @@ static cJSON *login_nsigner(cJSON *params) {
} else { } else {
strncpy(host, device, sizeof(host) - 1); 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"; transport_name = "tcp";
} else if (strcmp(transport, "qrexec") == 0) { } else if (strcmp(transport, "qrexec") == 0) {
const char *svc = (service && service[0]) ? service : "qubes.NsignerRpc"; 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"; transport_name = "qrexec";
} else { } else {
sigprocmask(SIG_SETMASK, &old_set, NULL); 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". */ * rejects the call with an auth error that surfaces as "code -310". */
key_store_nsigner_set_default_auth(signer); 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) { if (rc != NOSTR_SUCCESS) {
nostr_signer_free(signer); nostr_signer_free(signer);
sigprocmask(SIG_SETMASK, &old_set, NULL); 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]; char pubkey_hex[65];
rc = nostr_signer_get_public_key(signer, pubkey_hex); rc = nostr_signer_get_public_key(signer, pubkey_hex);
if (rc != NOSTR_SUCCESS) { 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", g_printerr("[agent-login] n_signer get_public_key failed: rc=%d (%s)\n",
rc, err_str ? err_str : "?"); rc, err_str ? err_str : "?");
nostr_signer_free(signer); nostr_signer_free(signer);
+2 -2
View File
@@ -132,7 +132,7 @@ const mcp_tool_def_t tool_defs[] = {
{"login", {"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.", "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", {"logout",
"Log out and clear the current Nostr identity.", "Log out and clear the current Nostr identity.",
@@ -140,7 +140,7 @@ const mcp_tool_def_t tool_defs[] = {
{"switch_identity", {"switch_identity",
"Switch to a new Nostr identity. Same parameters as login (including 'generate'). Frees the old signer first.", "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 */ /* Navigation tools */
{"open", {"open",
+9
View File
@@ -63,6 +63,7 @@ void cli_print_usage(FILE *fp) {
" --nsigner-transport <t> (nsigner) serial|unix|tcp|qrexec\n" " --nsigner-transport <t> (nsigner) serial|unix|tcp|qrexec\n"
" --nsigner-device <path> (nsigner) device path / socket / host:port / qube\n" " --nsigner-device <path> (nsigner) device path / socket / host:port / qube\n"
" --nsigner-service <name> (nsigner) qrexec service name\n" " --nsigner-service <name> (nsigner) qrexec service name\n"
" --nsigner-role <name> (nsigner) role name (default: nostr_range)\n"
" --nsigner-index <n> (nsigner) key index (default 0)\n" " --nsigner-index <n> (nsigner) key index (default 0)\n"
" --login-timeout <ms> Override agent login timeout (GTK dialog path)\n" " --login-timeout <ms> Override agent login timeout (GTK dialog path)\n"
"\n" "\n"
@@ -103,6 +104,7 @@ enum {
OPT_NSIGNER_TRANSPORT, OPT_NSIGNER_TRANSPORT,
OPT_NSIGNER_DEVICE, OPT_NSIGNER_DEVICE,
OPT_NSIGNER_SERVICE, OPT_NSIGNER_SERVICE,
OPT_NSIGNER_ROLE,
OPT_NSIGNER_INDEX, OPT_NSIGNER_INDEX,
OPT_NO_LOGIN, OPT_NO_LOGIN,
OPT_LOGIN_TIMEOUT, OPT_LOGIN_TIMEOUT,
@@ -137,6 +139,7 @@ static struct option long_opts[] = {
{"nsigner-transport", required_argument, 0, OPT_NSIGNER_TRANSPORT}, {"nsigner-transport", required_argument, 0, OPT_NSIGNER_TRANSPORT},
{"nsigner-device", required_argument, 0, OPT_NSIGNER_DEVICE}, {"nsigner-device", required_argument, 0, OPT_NSIGNER_DEVICE},
{"nsigner-service", required_argument, 0, OPT_NSIGNER_SERVICE}, {"nsigner-service", required_argument, 0, OPT_NSIGNER_SERVICE},
{"nsigner-role", required_argument, 0, OPT_NSIGNER_ROLE},
{"nsigner-index", required_argument, 0, OPT_NSIGNER_INDEX}, {"nsigner-index", required_argument, 0, OPT_NSIGNER_INDEX},
{"no-login", no_argument, 0, OPT_NO_LOGIN}, {"no-login", no_argument, 0, OPT_NO_LOGIN},
{"login-timeout", required_argument, 0, OPT_LOGIN_TIMEOUT}, {"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_transport);
g_free(args->nsigner_device); g_free(args->nsigner_device);
g_free(args->nsigner_service); g_free(args->nsigner_service);
g_free(args->nsigner_role);
g_free(args->download_url); g_free(args->download_url);
memset(args, 0, sizeof(*args)); 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); g_free(out->nsigner_service);
out->nsigner_service = xstrdup(optarg); out->nsigner_service = xstrdup(optarg);
break; break;
case OPT_NSIGNER_ROLE:
g_free(out->nsigner_role);
out->nsigner_role = xstrdup(optarg);
break;
case OPT_NSIGNER_INDEX: case OPT_NSIGNER_INDEX:
out->nsigner_index = atoi(optarg); out->nsigner_index = atoi(optarg);
if (out->nsigner_index < 0) { 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_transport) cJSON_AddStringToObject(params, "transport", args->nsigner_transport);
if (args->nsigner_device) cJSON_AddStringToObject(params, "device", args->nsigner_device); 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_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); if (args->nsigner_index >= 0) cJSON_AddNumberToObject(params, "index", args->nsigner_index);
/* agent_login() calls app_set_signer() on success, which sets the /* agent_login() calls app_set_signer() on success, which sets the
+1
View File
@@ -61,6 +61,7 @@ typedef struct {
char *nsigner_transport; /* --nsigner-transport */ char *nsigner_transport; /* --nsigner-transport */
char *nsigner_device; /* --nsigner-device */ char *nsigner_device; /* --nsigner-device */
char *nsigner_service; /* --nsigner-service */ char *nsigner_service; /* --nsigner-service */
char *nsigner_role; /* --nsigner-role (default: "nostr_range") */
int nsigner_index; /* --nsigner-index (-1 = unset) */ int nsigner_index; /* --nsigner-index (-1 = unset) */
int login_timeout_ms; /* --login-timeout (0 = unset) */ int login_timeout_ms; /* --login-timeout (0 = unset) */
+9 -5
View File
@@ -60,10 +60,11 @@ nostr_signer_t *key_store_create_signer(const key_store_identity_t *identity) {
case KEY_STORE_METHOD_NSIGNER: { case KEY_STORE_METHOD_NSIGNER: {
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) #if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
nostr_signer_t *signer = NULL; nostr_signer_t *signer = NULL;
const char *role = identity->nsigner_role[0] ? identity->nsigner_role : "nostr_range";
if (strcmp(identity->nsigner_transport, "serial") == 0) { 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) { } 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) { } else if (strcmp(identity->nsigner_transport, "tcp") == 0) {
/* nsigner_device is "host:port" */ /* nsigner_device is "host:port" */
char host[256]; 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); memcpy(host, identity->nsigner_device, hlen);
host[hlen] = '\0'; host[hlen] = '\0';
port = atoi(sep + 1); 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) { } 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) { 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 /* n_signer's serial and TCP transports require a kind-27235 auth
* envelope on every request. Install the default caller identity * envelope on every request. Install the default caller identity
+2 -1
View File
@@ -50,7 +50,8 @@ typedef struct {
/* n_signer transport (nsigner method only). */ /* n_signer transport (nsigner method only). */
char nsigner_transport[16]; /* "serial" | "unix" | "tcp" | "qrexec" */ char nsigner_transport[16]; /* "serial" | "unix" | "tcp" | "qrexec" */
char nsigner_device[256]; /* device path / socket name / host:port / qube */ 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; } key_store_identity_t;
/* /*
+52 -29
View File
@@ -454,12 +454,17 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) {
GtkWidget *screen = get_method_page(ctx, "nsigner"); GtkWidget *screen = get_method_page(ctx, "nsigner");
GtkWidget *transport_combo = g_object_get_data(G_OBJECT(screen), "transport-combo"); 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 *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"); 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 *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)); 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)); 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 /* Block SIGCHLD during n_signer calls — the qrexec transport spawns
* child processes, and SIGCHLD can interrupt gtk_dialog_run's * child processes, and SIGCHLD can interrupt gtk_dialog_run's
* internal main loop, causing the dialog to close prematurely. */ * 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"; const char *transport_name = "serial";
if (transport_idx == 0) { if (transport_idx == 0) {
signer = nostr_signer_nsigner_serial(device, NULL, 15000); signer = nostr_signer_nsigner_serial(device, role, 15000);
transport_name = "serial"; transport_name = "serial";
} else if (transport_idx == 1) { } else if (transport_idx == 1) {
signer = nostr_signer_nsigner_unix(device, NULL, 15000); signer = nostr_signer_nsigner_unix(device, role, 15000);
transport_name = "unix"; transport_name = "unix";
} else if (transport_idx == 2) { } else if (transport_idx == 2) {
char host[256]; char host[256];
@@ -494,7 +499,7 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) {
memcpy(host, device, hlen); memcpy(host, device, hlen);
host[hlen] = '\0'; host[hlen] = '\0';
port = atoi(sep + 1); 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"; 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"); 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"; const char *service = service_entry ? gtk_entry_get_text(GTK_ENTRY(service_entry)) : "qubes.NsignerRpc";
if (service[0] == '\0') service = "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"; transport_name = "qrexec";
} }
@@ -521,10 +526,14 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) {
* "code -310". */ * "code -310". */
key_store_nsigner_set_default_auth(signer); 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) { if (rc_idx != NOSTR_SUCCESS) {
gtk_label_set_text(GTK_LABEL(ctx->status_label), 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); nostr_signer_free(signer);
sigprocmask(SIG_SETMASK, &old_set, NULL); sigprocmask(SIG_SETMASK, &old_set, NULL);
return; return;
@@ -532,29 +541,26 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) {
char pubkey_hex[65]; char pubkey_hex[65];
int rc_pk = nostr_signer_get_public_key(signer, pubkey_hex); 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) { if (rc_pk != NOSTR_SUCCESS) {
char errmsg[320]; char errmsg[320];
const char *desc = "unknown error"; const char *desc = nostr_signer_last_error(signer);
switch (rc_pk) { if (!desc || desc[0] == '\0' || strcmp(desc, "no error details available") == 0) {
case -5: desc = "I/O failed — signer may have denied the request or disconnected"; break; /* Fall back to error code mapping */
case -2001: desc = "policy denied — caller not approved at signer terminal"; break; switch (rc_pk) {
case -2002: desc = "index not in signer's whitelist"; break; case -5: desc = "I/O failed — signer may have denied the request or disconnected"; break;
case -3: desc = "crypto operation failed"; break; case -2001: desc = "policy denied — caller not approved at signer terminal"; break;
case NOSTR_ERROR_NIP46_AUTH_CHALLENGE: case -2002: desc = "index not in signer's whitelist"; break;
/* Device RPC codes 2010-2017: auth-related rejection. case -3: desc = "crypto operation failed"; break;
* Most commonly this means the kind-27235 caller auth case NOSTR_ERROR_NIP46_AUTH_CHALLENGE:
* envelope was missing/rejected (the default caller desc = "auth rejected by n_signer (code -310). The caller "
* identity is installed automatically, but the device's "auth envelope was missing or denied, or the device "
* policy may still deny it). It can also mean the device "is requesting on-device approval. Confirm any "
* is requesting on-device approval (button press / TUI "prompt on the n_signer hardware, then click Sign "
* confirm) for this operation. */ "In again";
desc = "auth rejected by n_signer (code -310). The caller " break;
"auth envelope was missing or denied, or the device " default: desc = "unknown error"; break;
"is requesting on-device approval. Confirm any " }
"prompt on the n_signer hardware, then click Sign "
"In again";
break;
default: break;
} }
snprintf(errmsg, sizeof(errmsg), snprintf(errmsg, sizeof(errmsg),
"n_signer error: %s (code %d). Try a different key index.", "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]; char npub[128];
pubkey_to_npub(pubkey_hex, npub); pubkey_to_npub(pubkey_hex, npub);
g_print("[login] n_signer: transport=%s index=%d pubkey=%s npub=%s\n", g_print("[login] n_signer: transport=%s role=%s index=%d pubkey=%s npub=%s\n",
transport_name, nostr_index, pubkey_hex, transport_name, role, nostr_index, pubkey_hex,
npub[0] ? npub : "(conversion failed)"); npub[0] ? npub : "(conversion failed)");
ctx->result->method = KEY_STORE_METHOD_NSIGNER; 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); sizeof(ctx->result->identity.nsigner_device) - 1);
ctx->result->identity.nsigner_device[sizeof(ctx->result->identity.nsigner_device) - 1] = '\0'; ctx->result->identity.nsigner_device[sizeof(ctx->result->identity.nsigner_device) - 1] = '\0';
ctx->result->identity.nsigner_index = nostr_index; 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; ctx->done = TRUE;
gtk_dialog_response(GTK_DIALOG(ctx->dialog), GTK_RESPONSE_ACCEPT); 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). */ /* Show the service field by default (qrexec is the default transport). */
gtk_widget_show_all(service_box); 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. */ /* Connect transport change callback to update the UI dynamically. */
g_signal_connect(transport_combo, "changed", g_signal_connect(transport_combo, "changed",
G_CALLBACK(on_transport_changed), box); 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), "enum-btn", enum_btn);
g_object_set_data(G_OBJECT(box), "service-box", service_box); 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), "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-spin", index_spin);
g_object_set_data(G_OBJECT(box), "index-label", index_label); g_object_set_data(G_OBJECT(box), "index-label", index_label);
return box; return box;
+2 -2
View File
@@ -11,9 +11,9 @@
#ifndef SOVEREIGN_BROWSER_VERSION_H #ifndef SOVEREIGN_BROWSER_VERSION_H
#define 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_MAJOR 0
#define SB_VERSION_MINOR 0 #define SB_VERSION_MINOR 0
#define SB_VERSION_PATCH 68 #define SB_VERSION_PATCH 69
#endif /* SOVEREIGN_BROWSER_VERSION_H */ #endif /* SOVEREIGN_BROWSER_VERSION_H */
@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SharedWorker Absolute URL Test</title>
<style>
body { font-family: sans-serif; background: #1a1a2e; color: #eee; padding: 40px; }
h1 { margin-bottom: 10px; }
.pass { color: #0f0; }
.fail { color: #f00; }
.info { color: #ff0; }
#output { background: #16213e; padding: 16px; border-radius: 8px; margin-top: 20px; font-family: monospace; white-space: pre-wrap; }
</style>
</head>
<body>
<h1>🔄 SharedWorker Absolute local:// importScripts Test</h1>
<p>Tests whether <code>importScripts('local:///absolute/path')</code> works inside a SharedWorker.</p>
<div id="output">Running tests...</div>
<script>
var output = document.getElementById('output');
function log(msg, cls) {
output.innerHTML += '<span class="' + (cls || '') + '">' + msg + '</span>\n';
}
log('=== Absolute local:// importScripts Test ===\n', 'info');
try {
var worker = new SharedWorker('shared-worker-absolute-test.js');
log('✓ SharedWorker constructor succeeded', 'pass');
worker.port.onmessage = function(e) {
log('✓ Received: ' + e.data, 'pass');
};
worker.port.onerror = function(e) {
log('✗ Worker error: ' + (e.message || 'unknown'), 'fail');
};
worker.port.postMessage('ping');
setTimeout(function() {
log('\n--- Done ---', 'info');
}, 3000);
} catch (e) {
log('✗ SharedWorker constructor threw: ' + e.message, 'fail');
}
</script>
</body>
</html>
@@ -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');
};
};