Files
sovereign_browser_rust/plans/nsigner-qrexec-login-fix.md
T

6.0 KiB

n_signer Remote-Qube Login Fix

Problem

Signing in via the n_signer tab with the "Other Qube" transport silently fails, while the equivalent CLI works:

signer-client --qrexec nostr_signer:qubes.SignerRpc --role nostr_range --path "m/44'/1237'/0'/0/0" get-public-key

Root Causes

  1. qrexec is rejected outright. on_login_clicked() in src/login_dialog.rs returns "This transport is not supported in this build" for the Other Qube transport (which is the dialog default), even though QrexecTransport exists in the nostr-signer crate. The Service field is never read by the handler at all.
  2. Wrong wire protocol in the nostr-signer crate. NsignerClient::call() sends params as a JSON object with no request id. The signer server's dispatcher requires params to be an array (options object as the last element) and rejects objects with INVALID_REQUEST. Trait methods also use wrong verb names: get_public_key instead of nostr_get_public_key, nip04_encrypt instead of nostr_nip04_encrypt, etc. Even UNIX/TCP logins can never work against this server.
  3. Errors are swallowed. All io errors map to NostrError::NetworkFailed discarding the real message; qrexec-client-vm stderr is sent to Stdio::null() (the working client inherits it); error responses are parsed as strings when the server sends {"code":N,"message":"..."} objects.

Wire Protocol — verified against /home/user/lt/signer/src/dispatcher.rs

Request: {"id":"1","method":...,"params":[...]} — params MUST be an array; the options object is the LAST array element.

Response: {"id":"...","result":...} or {"id":"...","error":{"code":N,"message":"..."}}

Operation Method Params
Nostr pubkey nostr_get_public_key [ {role, role_path} ]
Sign event nostr_sign_event [ event_json, opts ]
NIP-04 enc/dec nostr_nip04_encrypt / nostr_nip04_decrypt [ peer_hex, text, opts ]
NIP-44 enc/dec nostr_nip44_encrypt / nostr_nip44_decrypt [ peer_hex, text, opts ]
Info get_info []
Alg pubkey get_public_key [ {algorithm, index} ]
Alg sign sign [ msg_hex, opts ]
Alg verify verify [ msg_hex, sig_hex, opts ]
X25519 ECDH derive_shared_secret [ peer_hex, opts ]
HMAC derive derive [ data, opts with index ]
ML-KEM encapsulate / decapsulate [ hex, opts ]
OTP encrypt / decrypt [ text, opts with algorithm otp ]

opts = {"role":"...","role_path":"..."} for nostr verbs; {"algorithm":"...","index":N} for algorithm verbs.

Request Flow

sequenceDiagram
    participant D as Login Dialog
    participant S as NsignerSigner
    participant Q as QrexecTransport
    participant V as qrexec-client-vm
    participant B as signer bridge on nostr_signer qube
    participant U as signer unix socket

    D->>S: from_transport + set_role_path
    D->>S: get_public_key
    S->>Q: call nostr_get_public_key
    Q->>V: spawn qrexec-client-vm nostr_signer qubes.SignerRpc
    V->>B: qrexec connection
    B->>U: preamble qrexec_source
    B->>U: framed JSON-RPC request
    U-->>B: framed response
    B-->>V: framed response
    V-->>Q: stdout
    Q-->>S: response JSON
    S-->>D: pubkey hex

One request per qrexec connection — the crate's reconnect-per-call model is correct.

Changes

nostr_core_lib_rust/signer/src/nsigner.rs

  • NsignerClient::call(): take Vec<Value> params; add an id counter; parse error objects (message + code) into last_error; eprintln! diagnostics on send/recv/parse failures.
  • NostrSigner impl on NsignerSigner: use nostr_* verbs with a selector-options helper that always includes role and includes role_path when set.
  • Algorithm methods: correct verb names (derive_shared_secret, encrypt/decrypt with algorithm otp), array params, structured result parsing (result.signature, result.shared_secret, result.valid, result.digest, ...). Adjust ml_dsa_verify to server semantics (server verifies against its own derived key).
  • QrexecTransport: inherit stderr from qrexec-client-vm; eprintln! the real spawn/io error messages.
  • UnixTransport: abstract-socket support via libc (mirroring the reference client's connect_abstract_unix) so the UNIX Socket option works against this server.
  • Update unit tests to assert the corrected wire protocol.

sovereign_browser_rust/src/login_dialog.rs

  • Service default qubes.NsignerRpc → qubes.SignerRpc.
  • Read the service field; wire QrexecTransport for Other Qube (transport index 3) and SerialTransport for USB Serial (index 0).
  • eprintln! diagnostics: connection parameters, failures with last_error, success with pubkey.

sovereign_browser_rust/src/key_store.rs

  • Add nsigner_service field to KeyStoreIdentity.
  • Implement the Nsigner arm of key_store_create_signer().

sovereign_browser_rust/src/main.rs and src/menu.rs

  • Print sign-in result (method + pubkey) to console in do_login().

Verification

  1. cargo test in nostr_core_lib_rust/signer.
  2. cargo build in sovereign_browser_rust.
  3. Manual: launch browser → n_signer tab → defaults → Sign In. Console shows the connection parameters and any errors; pubkey matches the CLI output 8ff74724....

Known Limitations

  • qrexec reads have no timeout (ChildStdout is blocking) — a hung service freezes the dialog; same limitation as the reference client.
  • The sign-in RPC runs on the GTK main thread (pre-existing design).
  • The crate's auth-envelope format does not match the server's NIP-42 event envelope; it is unused (browser never calls set_auth, server auth is off) — out of scope.