Files

10 KiB

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:

/* 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:

/* 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():

/* 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():

/* 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():

/* 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:

{
  "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:

/* 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