# Agent Client Reference — n_signer This document is the complete reference for **agents, AI tools, and automated processes** that need to call [`n_signer`](../README.md) — a signing oracle that holds BIP-39 keys in locked memory and exposes them over multiple transports. ## Table of Contents 1. [Architecture overview](#1-architecture-overview) 2. [Wire protocol (all transports)](#2-wire-protocol-all-transports) 3. [Transport: Qubes qrexec (recommended for cross-qube)](#3-transport-qubes-qrexec-recommended-for-cross-qube) 4. [Transport: Local Unix abstract socket](#4-transport-local-unix-abstract-socket) 5. [Transport: TCP (FIPS mesh)](#5-transport-tcp-fips-mesh) 6. [Transport: HTTP](#6-transport-http) 7. [Transport: USB/serial (hardware signers)](#7-transport-usbserial-hardware-signers) 8. [Transport: Stdio (one-shot)](#8-transport-stdio-one-shot) 9. [Auth envelope (kind-27235)](#9-auth-envelope-kind-27235) 10. [Complete verb reference](#10-complete-verb-reference) 11. [Algorithm reference](#11-algorithm-reference) 12. [Error codes](#12-error-codes) 13. [End-to-end: publish a Nostr event](#13-end-to-end-publish-a-nostr-event) 14. [End-to-end: sign arbitrary data](#14-end-to-end-sign-arbitrary-data) 15. [End-to-end: PQ KEM encapsulate/decapsulate](#15-end-to-end-pq-kem-encapsulatedecapsulate) 16. [Discovery: finding running signers](#16-discovery-finding-running-signers) 17. [Prerequisites and setup](#17-prerequisites-and-setup) 18. [Reference files in this repo](#18-reference-files-in-this-repo) --- ## 1. Architecture overview ``` ┌─────────────────────────────────────────────────────────────┐ │ Caller (agent) │ │ Python / Node / C / Shell / any language │ │ Builds framed JSON-RPC → sends over transport → reads resp │ └──────────┬──────────────────────────────────────┬───────────┘ │ │ ┌─────┴──────┐ ┌──────────┴──────────┐ │ qrexec │ │ TCP / HTTP / Unix │ │ (Qubes) │ │ Socket / USB │ └─────┬──────┘ └──────────┬──────────┘ │ │ ┌─────┴──────────────────────────────────────┴───────────┐ │ n_signer │ │ BIP-39 mnemonic in mlock'd RAM │ │ Derives keys on demand (secp256k1, ed25519, PQ, etc.) │ │ Enforces policy + approval per caller │ └─────────────────────────────────────────────────────────┘ ``` The signer is a **single foreground process** attached to a terminal. It holds the mnemonic in locked memory only — nothing touches disk. When the process exits, all state is destroyed. --- ## 2. Wire protocol (all transports) Every transport uses the same framing and JSON-RPC contract. ### 2.1 Framing ``` [4 bytes: big-endian payload length N][N bytes: UTF-8 JSON payload] ``` - Length prefix is `uint32_t` in network byte order. - Payload is exactly `N` bytes of UTF-8 JSON. - One request frame → one response frame per connection (except HTTP which uses standard HTTP request/response). ### 2.2 Request shape ```json { "id": "", "method": "", "params": [ , , ..., { } ] } ``` - `id` — any string. Used to match async responses. Echoed verbatim. - `method` — one of the verbs in §10. - `params` — JSON array. Positional args first; last element is conventionally an options object. ### 2.3 Response shape Success: ```json { "id": "", "result": "" } ``` Error: ```json { "id": "", "error": { "code": , "message": "" } } ``` `result` is always a JSON string. For structured verbs (like `get_public_key` with algorithm), the string is itself serialized JSON — parse it again. ### 2.4 Auth envelope (required for TCP/HTTP, optional for qrexec) TCP and HTTP listeners require a kind-27235 auth envelope in the `auth` field. See [§9](#9-auth-envelope-kind-27235). --- ## 3. Transport: Qubes qrexec (recommended for cross-qube) **Best for**: Agents running in a caller qube, talking to n_signer in a dedicated signer qube. **Auth**: Not required — caller identity comes from `QREXEC_REMOTE_DOMAIN` as `qubes:`. ### 3.1 Shell one-liner ```bash # Get public key for nostr_index 0 printf '\x00\x00\x00\x3f'"$(echo '{"id":"1","method":"get_public_key","params":[{"nostr_index":0}]}')" | \ qrexec-client-vm nostr_signer qubes.NsignerRpc | tail -c +5 ``` ### 3.2 Python (stdlib only) ```python import json, struct, subprocess def call_nsigner(target_qube, request): """Send one framed JSON-RPC request via qrexec, return parsed response.""" payload = json.dumps(request, separators=(",", ":")).encode("utf-8") frame = struct.pack(">I", len(payload)) + payload proc = subprocess.Popen( ["qrexec-client-vm", target_qube, "qubes.NsignerRpc"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) out, err = proc.communicate(frame) if proc.returncode != 0: raise RuntimeError(f"qrexec failed: {err.decode()}") length = struct.unpack(">I", out[:4])[0] return json.loads(out[4:4+length]) ``` Full demo: [`client/demo_python.py`](../client/demo_python.py) ### 3.3 Node.js ```javascript const { spawn } = require("child_process"); function callNsigner(targetQube, request) { return new Promise((resolve, reject) => { const payload = Buffer.from(JSON.stringify(request), "utf8"); const header = Buffer.alloc(4); header.writeUInt32BE(payload.length, 0); const framed = Buffer.concat([header, payload]); const proc = spawn("qrexec-client-vm", [targetQube, "qubes.NsignerRpc"], { stdio: ["pipe", "pipe", "pipe"], }); const chunks = []; proc.stdout.on("data", (c) => chunks.push(c)); proc.on("close", (code) => { if (code !== 0) return reject(new Error(`exit code ${code}`)); const buf = Buffer.concat(chunks); const len = buf.readUInt32BE(0); resolve(JSON.parse(buf.subarray(4, 4 + len).toString())); }); proc.stdin.write(framed); proc.stdin.end(); }); } ``` Full demo: [`client/demo_javascript.js`](../client/demo_javascript.js) ### 3.4 C (using nostr_core_lib) ```c #include "nostr_signer.h" nostr_signer_t *signer = nostr_signer_nsigner_qrexec("nostr_signer", "qubes.NsignerRpc", NULL, 30000); nostr_signer_nsigner_set_nostr_index(signer, 0); char pubkey[65]; nostr_signer_get_public_key(signer, pubkey); ``` Full demo: [`client/demo_c99.c`](../client/demo_c99.c) ### 3.5 Prerequisites 1. n_signer running in the signer qube: `nsigner --listen unix --socket-name nsigner --bridge-source-trusted` 2. qrexec service installed at `/etc/qubes-rpc/qubes.NsignerRpc`: `exec nsigner bridge --to nsigner` 3. dom0 policy allowing caller qube → signer qube (see [`packaging/qubes/policy.d/40-nsigner.policy`](../packaging/qubes/policy.d/40-nsigner.policy)) --- ## 4. Transport: Local Unix abstract socket **Best for**: Agents on the same machine as the signer. **Auth**: Not required — identity from `SO_PEERCRED` (kernel-verified UID/PID). ### 4.1 Discovery ```bash nsigner list # Output: @nsigner, @nsigner_hairy_dog, etc. ``` ### 4.2 Using the nsigner CLI ```bash nsigner client '{"id":"1","method":"get_public_key","params":[{"nostr_index":0}]}' --socket-name nsigner ``` ### 4.3 C (using nostr_core_lib) ```c #include "nsigner_transport.h" #include "nsigner_client.h" nsigner_transport_t *t = nsigner_transport_open_unix("nsigner", 5000); nsigner_client_t *c = nsigner_client_new(t); cJSON *params = cJSON_CreateArray(); cJSON *result = NULL; nsigner_client_call(c, "nostr_get_public_key", params, &result); ``` See [`examples/get_public_key_client.c`](../examples/get_public_key_client.c). ### 4.4 Python (raw socket) ```python import json, socket, struct sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.connect("\0nsigner") # \0 prefix for abstract namespace payload = json.dumps({"id":"1","method":"get_public_key","params":[{"nostr_index":0}]}).encode() sock.sendall(struct.pack(">I", len(payload)) + payload) hdr = sock.recv(4) length = struct.unpack(">I", hdr)[0] body = sock.recv(length) print(json.loads(body)) ``` --- ## 5. Transport: TCP (FIPS mesh) **Best for**: Cross-qube or network callers where qrexec is not available. **Auth**: **Required** — kind-27235 auth envelope (see [§9](#9-auth-envelope-kind-27235)). ### 5.1 Python (with coincurve) ```python import hashlib, json, socket, struct, time from coincurve import PrivateKey HOST, PORT = "192.168.1.100", 11111 CALLER_PRIVKEY = bytes(range(1, 33)) # Replace with your key params = [{"nostr_index": 0}] body_hash = hashlib.sha256(json.dumps(params, separators=(",",":")).encode()).hexdigest() sk = PrivateKey(CALLER_PRIVKEY) pubkey_x = sk.public_key.format(compressed=False)[1:33].hex() created_at = int(time.time()) tags = [["nsigner_rpc","1"],["nsigner_method","get_public_key"],["nsigner_body_hash",body_hash]] serialized = json.dumps([0, pubkey_x, created_at, 27235, tags, "tcp-agent"], separators=(",",":")).encode() event_id = hashlib.sha256(serialized).hexdigest() sig = sk.sign_schnorr(bytes.fromhex(event_id), aux_randomness=b"\x00"*32).hex() request = { "id": "1", "method": "get_public_key", "params": params, "auth": {"id": event_id, "pubkey": pubkey_x, "created_at": created_at, "kind": 27235, "tags": tags, "content": "tcp-agent", "sig": sig}, } payload = json.dumps(request, separators=(",",":")).encode() frame = struct.pack(">I", len(payload)) + payload with socket.create_connection((HOST, PORT), timeout=10) as s: s.sendall(frame) hdr = s.recv(4) ln = struct.unpack(">I", hdr)[0] body = s.recv(ln) print(json.loads(body)) ``` See [`examples/get_pubkey_fips.py`](../examples/get_pubkey_fips.py) and [`examples/n_signer_qube_example_fips.js`](../examples/n_signer_qube_example_fips.js). ### 5.2 C (using nostr_core_lib) ```c nsigner_transport_t *t = nsigner_transport_open_tcp("192.168.1.100", 11111, 10000); nsigner_client_t *c = nsigner_client_new(t); nsigner_client_set_auth(c, caller_privkey, "tcp-agent"); // ... call as usual ``` See [`examples/get_pubkey_tcp.c`](../examples/get_pubkey_tcp.c). --- ## 6. Transport: HTTP **Best for**: curl-friendly callers, REST clients. **Auth**: **Required** — kind-27235 auth envelope in the JSON body. ```bash curl -X POST http://192.168.1.100:11112/ \ -H "Content-Type: application/json" \ -d '{ "id":"1", "method":"get_public_key", "params":[{"nostr_index":0}], "auth":{...} }' ``` The HTTP listener uses the same framing internally but presents a standard HTTP interface. One request per connection. --- ## 7. Transport: USB/serial (hardware signers) **Best for**: Embedded/air-gap signers (Feather S3, Teensy 4.1, CYD ESP32). **Auth**: Not required — physical possession is the trust anchor. ### 7.1 Python (pyserial) ```python import json, struct, serial ser = serial.Serial("/dev/ttyACM0", 115200, timeout=5) payload = json.dumps({"id":"1","method":"get_public_key","params":[{"nostr_index":0}]}).encode() ser.write(struct.pack(">I", len(payload)) + payload) hdr = ser.read(4) length = struct.unpack(">I", hdr)[0] body = ser.read(length) print(json.loads(body)) ``` See [`examples/feather_get_public_key.py`](../examples/feather_get_public_key.py) and [`examples/feather_sign_event.py`](../examples/feather_sign_event.py). --- ## 8. Transport: Stdio (one-shot) **Best for**: Scripted one-off calls via pipe. The signer can be started with `--listen stdio` to handle exactly one framed request on stdin and write one framed response to stdout. ```bash echo -n '' | nsigner --listen stdio --allow-all ``` Or via the qrexec bridge service (which uses stdio internally to relay to the persistent signer's Unix socket). --- ## 9. Auth envelope (kind-27235) TCP and HTTP listeners require a **kind-27235 Nostr event** in the `auth` field of every request. This proves the caller controls a keypair. ### 9.1 Auth envelope structure ```json { "auth": { "id": "", "pubkey": "", "created_at": , "kind": 27235, "tags": [ ["nsigner_rpc", ""], ["nsigner_method", ""], ["nsigner_body_hash", ""] ], "content": "", "sig": "" } } ``` ### 9.2 Building the auth envelope (pseudocode) ``` 1. Compute body_hash = SHA256(JSON.stringify(params, separators=(",",":"))) 2. Build tags: [["nsigner_rpc", id], ["nsigner_method", method], ["nsigner_body_hash", body_hash]] 3. Serialize event for signing: JSON.stringify([0, pubkey, created_at, 27235, tags, content]) 4. Compute event_id = SHA256(serialized) 5. Sign event_id with caller's secp256k1 key (Schnorr/BIP-340) 6. Include full event as the "auth" field in the request ``` ### 9.3 Validation rules (server-side) | Check | Error code | |-------|-----------| | Missing `auth` field | 2014 `auth_envelope_required` | | Malformed auth JSON | 2010 `auth_envelope_malformed` | | `kind` != 27235 | 2013 `auth_kind_invalid` | | Signature doesn't verify | 2012 `auth_signature_invalid` | | `nsigner_rpc` tag != request `id` | 2011 `auth_body_mismatch` | | `nsigner_method` tag != request `method` | 2011 `auth_body_mismatch` | | `nsigner_body_hash` != SHA256(params) | 2011 `auth_body_mismatch` | | Timestamp skew > 300 seconds | 2015 `auth_envelope_mismatch` | | Replay (same event_id seen before) | 2015 `auth_envelope_mismatch` | ### 9.4 Python example (full) See [`examples/get_pubkey_fips.py`](../examples/get_pubkey_fips.py) for a complete working example. ### 9.5 JavaScript example (full) See [`examples/n_signer_qube_example_fips.js`](../examples/n_signer_qube_example_fips.js) for a complete working example. --- ## 10. Complete verb reference ### 10.1 Algorithm-based verbs These select a key by `algorithm` + `index` (see [§11](#11-algorithm-reference) for derivation paths). | Verb | Algorithms | Params | Options | |------|-----------|--------|---------| | `get_public_key` | All key-deriving | `[]` | `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` | | `derive` | secp256k1 | `[]` | `algorithm`, `index` (required) | | `encrypt` | otp | `[]` | `algorithm`, `encoding` | | `decrypt` | otp | `[]` | `algorithm`, `encoding` | \* `scheme`: `"schnorr"` (default) or `"ecdsa"` for secp256k1. ### 10.2 Nostr protocol verbs These select a secp256k1 NIP-06 key via `nostr_index` (or `role`/`role_path`). | Verb | Params | Options | |------|--------|---------| | `nostr_get_public_key` | `[]` | `nostr_index`, `format` | | `nostr_sign_event` | `[]` | `nostr_index` | | `nostr_mine_event` | `[]` | `nostr_index`, `difficulty`, `timeout_sec`, `threads` | | `nostr_nip04_encrypt` | `[, ]` | `nostr_index` | | `nostr_nip04_decrypt` | `[<peer_pubkey_hex>, <ciphertext>]` | `nostr_index` | | `nostr_nip44_encrypt` | `[<peer_pubkey_hex>, <plaintext>]` | `nostr_index` | | `nostr_nip44_decrypt` | `[<peer_pubkey_hex>, <ciphertext>]` | `nostr_index` | ### 10.3 Metadata | Verb | Params | Description | |------|--------|-------------| | `get_info` | `[]` | Returns signer metadata (name, version, supported verbs/algorithms). Safe to call before mnemonic is loaded. | ### 10.4 Example requests ```json // Get public key for ML-DSA-65 index 0 {"id":"1","method":"get_public_key","params":[{"algorithm":"ml-dsa-65","index":0}]} // Sign "hello" with ed25519 index 0 {"id":"2","method":"sign","params":["68656c6c6f",{"algorithm":"ed25519","index":0}]} // Sign a Nostr event with nostr_index 0 {"id":"3","method":"nostr_sign_event","params":["{\"kind\":1,\"content\":\"Hello\",\"tags\":[],\"created_at\":1700000000,\"pubkey\":\"<hex>\"}",{"nostr_index":0}]} // NIP-44 encrypt {"id":"4","method":"nostr_nip44_encrypt","params":["<peer_pubkey_hex>","secret message",{"nostr_index":0}]} // ML-KEM-768 encapsulate {"id":"5","method":"encapsulate","params":["<peer_pubkey_hex>",{"algorithm":"ml-kem-768"}]} // Derive HMAC-SHA256(privkey, data) for opaque identifiers {"id":"6","method":"derive","params":["<data_hex>",{"algorithm":"secp256k1","index":0}]} ``` --- ## 11. Algorithm reference | Algorithm | Key type | FIPS | Derivation path | Pubkey | Privkey | Signature | |-----------|----------|------|-----------------|--------|---------|-----------| | `secp256k1` | Signature (Nostr) | — | `m/44'/1237'/<n>'/0/0` | 32 B | 32 B | 64 B | | `ed25519` | Signature (SSH) | — | `m/44'/102001'/<n>'/0/0'` | 32 B | 32 B | 64 B | | `x25519` | Key agreement (age) | — | `m/44'/102002'/<n>'/0/0'` | 32 B | 32 B | — | | `ml-dsa-65` | PQ signature | FIPS 204 | DRBG from seed | 1952 B | 4032 B | 3309 B | | `slh-dsa-128s` | PQ hash-based sig | FIPS 205 | DRBG from seed | 32 B | 64 B | 7856 B | | `ml-kem-768` | PQ KEM | FIPS 203 | DRBG from seed | 1184 B | 2400 B | — | | `otp` | One-time pad | — | USB pad (no derivation) | — | — | — | ### 11.1 `get_public_key` response format Algorithm-based `get_public_key` returns structured JSON: ```json {"algorithm":"ml-dsa-65","public_key":"<hex>","key_id":"<first 16 hex chars of pubkey>"} ``` Nostr `nostr_get_public_key` returns a plain 64-hex-char string by default, or structured JSON with `{"format":"structured"}`. --- ## 12. Error codes | Code | Message | Meaning | |------|---------|---------| | -32700 | `parse_error` | Request is not valid JSON | | -32600 | `invalid_request` | Missing `id`, `method`, or `params` | | -32601 | `method_not_found` | Unknown verb | | -32602 | `invalid_params` | Malformed arguments | | 1001 | `ambiguous_role_selector` | Multiple role selectors given | | 1002 | `unknown_role` | No role matched selector | | 1003 | `no_default_role` | No selector and no `main` role | | 1004 | `purpose_mismatch` | Role purpose not valid for verb | | 1005 | `curve_mismatch` | Role curve not valid for verb | | 1006 | `mnemonic_not_loaded` | No mnemonic loaded | | 1007 | `no_termination_condition` | `nostr_mine_event` without difficulty/timeout | | 1008 | `mining_failed` | Internal PoW error | | 1009 | `not_yet_implemented` | Verb+algorithm not yet implemented | | 1010 | `algorithm_not_supported_for_verb` | Algorithm not valid for verb | | 2010 | `auth_envelope_malformed` | Auth JSON is malformed | | 2011 | `auth_body_mismatch` | Auth tags don't match request | | 2012 | `auth_signature_invalid` | Auth signature doesn't verify | | 2013 | `auth_kind_invalid` | Auth kind != 27235 | | 2014 | `auth_envelope_required` | Auth missing (TCP/HTTP) | | 2015 | `auth_envelope_mismatch` | Timestamp skew or replay | --- ## 13. End-to-end: publish a Nostr event This is the most common agent task. Here's the complete flow using qrexec (simplest cross-qube transport): ```python #!/usr/bin/env python3 """Publish a Nostr event via n_signer over qrexec.""" import json, struct, subprocess, sys, time def call_nsigner(target_qube, request): payload = json.dumps(request, separators=(",", ":")).encode() frame = struct.pack(">I", len(payload)) + payload proc = subprocess.Popen( ["qrexec-client-vm", target_qube, "qubes.NsignerRpc"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) out, _ = proc.communicate(frame) length = struct.unpack(">I", out[:4])[0] return json.loads(out[4:4+length]) target = sys.argv[1] if len(sys.argv) > 1 else "nostr_signer" idx = int(sys.argv[2]) if len(sys.argv) > 2 else 0 # Step 1: Get public key pub = call_nsigner(target, {"id":"1","method":"get_public_key","params":[{"nostr_index":idx}]})["result"] print(f"Pubkey: {pub}") # Step 2: Build and sign event event = { "kind": 1, "content": "Hello from my agent!", "created_at": int(time.time()), "tags": [], "pubkey": pub, } result = call_nsigner(target, { "id": "2", "method": "nostr_sign_event", "params": [json.dumps(event, separators=(",",":")), {"nostr_index": idx}], }) signed = json.loads(result["result"]) print(f"Event ID: {signed['id']}") print(f"Signature: {signed['sig']}") # Step 3: Broadcast to relay(s) # signed is a complete Nostr event with id and sig — send to any relay ``` --- ## 14. End-to-end: sign arbitrary data Using algorithm-based verbs (any algorithm, any index): ```python # Sign "hello" with ed25519 index 0 result = call_nsigner(target, { "id": "1", "method": "sign", "params": ["68656c6c6f", {"algorithm": "ed25519", "index": 0}], }) signature = result["result"] # hex string # Verify result = call_nsigner(target, { "id": "2", "method": "verify", "params": ["68656c6c6f", signature, {"algorithm": "ed25519", "index": 0}], }) print(f"Verified: {result['result']}") # "true" or "false" ``` --- ## 15. End-to-end: PQ KEM encapsulate/decapsulate ```python # Get ML-KEM-768 public key for index 0 pub_result = call_nsigner(target, { "id": "1", "method": "get_public_key", "params": [{"algorithm": "ml-kem-768", "index": 0}], }) pub_info = json.loads(pub_result["result"]) print(f"ML-KEM-768 pubkey: {pub_info['public_key'][:32]}...") # Encapsulate (generate a shared secret + ciphertext for that pubkey) enc_result = call_nsigner(target, { "id": "2", "method": "encapsulate", "params": [pub_info["public_key"], {"algorithm": "ml-kem-768"}], }) enc_data = json.loads(enc_result["result"]) print(f"Ciphertext: {enc_data['ciphertext'][:32]}...") print(f"Shared secret: {enc_data['shared_secret'][:32]}...") # Decapsulate (recover shared secret from ciphertext using private key) dec_result = call_nsigner(target, { "id": "3", "method": "decapsulate", "params": [enc_data["ciphertext"], {"algorithm": "ml-kem-768", "index": 0}], }) dec_data = json.loads(dec_result["result"]) print(f"Decapsulated secret: {dec_data['shared_secret'][:32]}...") assert dec_data["shared_secret"] == enc_data["shared_secret"] print("✓ KEM round-trip verified") ``` --- ## 16. Discovery: finding running signers ### 16.1 List running signers ```bash nsigner list ``` Output (one per line): ``` @nsigner @nsigner_hairy_dog @nsigner_brave_canyon ``` ### 16.2 Check if a specific signer is running ```bash nsigner list | grep -q @nsigner && echo "running" || echo "not running" ``` ### 16.3 Programmatic discovery (Python) ```python import subprocess result = subprocess.run(["nsigner", "list"], capture_output=True, text=True) signers = [s.strip() for s in result.stdout.split("\n") if s.strip()] print(f"Found {len(signers)} signer(s): {signers}") ``` --- ## 17. Prerequisites and setup ### 17.1 On the signer qube ```bash # Install nsigner # Start the persistent signer nsigner --listen unix --socket-name nsigner --bridge-source-trusted # Install qrexec service sudo cp packaging/qubes/rpc/qubes.NsignerRpc /etc/qubes-rpc/qubes.NsignerRpc sudo chmod 0755 /etc/qubes-rpc/qubes.NsignerRpc ``` ### 17.2 In dom0 ```bash # Install policy sudo cp packaging/qubes/policy.d/40-nsigner.policy /etc/qubes/policy.d/40-nsigner.policy # Tag the signer qube qvm-tags nostr_signer add nsigner-signer ``` ### 17.3 On the caller qube No special setup needed — just `qrexec-client-vm` (pre-installed in all Qubes templates). --- ## 18. Reference files in this repo | File | What it shows | |------|---------------| | [`client/demo_python.py`](../client/demo_python.py) | Full Python demo (qrexec, stdlib only) — get_public_key, sign_event, nip44, mine_event | | [`client/demo_javascript.js`](../client/demo_javascript.js) | Full Node.js demo (qrexec) — same operations | | [`client/demo_c99.c`](../client/demo_c99.c) | Full C99 demo (qrexec, nostr_core_lib) — same operations | | [`examples/get_pubkey_fips.py`](../examples/get_pubkey_fips.py) | Minimal TCP/FIPS with auth envelope (Python) | | [`examples/get_pubkey_qrexec.c`](../examples/get_pubkey_qrexec.c) | Minimal qrexec in C | | [`examples/get_pubkey_tcp.c`](../examples/get_pubkey_tcp.c) | Minimal TCP in C with auth envelope | | [`examples/n_signer_qube_example_fips.js`](../examples/n_signer_qube_example_fips.js) | TCP/FIPS in Node.js with auth envelope | | [`examples/n_signer_qube_example_qrexec.js`](../examples/n_signer_qube_example_qrexec.js) | qrexec in Node.js | | [`examples/get_public_key_client.c`](../examples/get_public_key_client.c) | Unix socket in C | | [`examples/sign_event_client.c`](../examples/sign_event_client.c) | Unix socket sign event in C | | [`examples/feather_get_public_key.py`](../examples/feather_get_public_key.py) | USB/serial hardware signer (Python) | | [`examples/feather_sign_event.py`](../examples/feather_sign_event.py) | USB/serial sign event (Python) | | [`examples/pq_sign_example.c`](../examples/pq_sign_example.c) | ML-DSA-65 sign in C | | [`examples/pq_kem_example.c`](../examples/pq_kem_example.c) | ML-KEM-768 encaps/decaps in C | | [`examples/ssh_sign_example.c`](../examples/ssh_sign_example.c) | ed25519 SSH sign in C | | [`documents/CLIENT_IMPLEMENTATION.md`](CLIENT_IMPLEMENTATION.md) | Full wire contract spec (733 lines) | | [`README.md`](../README.md) | Full n_signer documentation (741 lines) | | [`packaging/qubes/setup_signer_qube.sh`](../packaging/qubes/setup_signer_qube.sh) | Automated signer qube setup | | [`.roo/n_signer_client.md`](../.roo/n_signer_client.md) | Roo skill file (agent-invocable) |