--- description: "Call n_signer to sign Nostr events, get public keys, encrypt/decrypt, and perform crypto operations across all transports (qrexec, unix socket, TCP, HTTP, USB/serial)." --- # n_signer Client Skill This skill tells an agent how to call [`n_signer`](../README.md) — a hardware/software signing oracle that holds BIP-39 keys in locked memory. The signer may be running on the same machine (Unix socket), in another Qubes qube (qrexec), on a hardware device (USB/serial), or reachable over TCP/HTTP. ## 1. Transport overview | Transport | Scope | Auth required | Best for | |-----------|-------|---------------|----------| | **qrexec** | Cross-qube (Qubes OS) | No (identity from `QREXEC_REMOTE_DOMAIN`) | Agents in caller qubes | | **Unix abstract socket** | Same machine | No (identity from `SO_PEERCRED`) | Local processes | | **TCP (FIPS mesh)** | Cross-qube or network | Yes (kind-27235 auth envelope) | Remote callers, FIPS networks | | **HTTP** | Cross-qube or network | Yes (kind-27235 auth envelope) | curl-friendly, REST clients | | **USB/serial** | Hardware signer (Feather, Teensy, CYD) | No (physical possession) | Embedded/air-gap | | **Stdio** | One-shot via pipe | No | Scripted one-off calls | ## 2. Wire protocol (all transports) Every request/response uses **length-prefixed framing**: ``` [4-byte big-endian payload length][UTF-8 JSON payload] ``` The JSON payload is a JSON-RPC 2.0-style object: ```json { "id": "", "method": "", "params": [, , ..., {}] } ``` Response (success): ```json { "id": "", "result": "" } ``` Response (error): ```json { "id": "", "error": { "code": , "message": "" } } ``` ## 3. Transport-specific invocation ### 3.1 Qubes qrexec (easiest cross-qube) ```bash # Pipe framed JSON-RPC through qrexec-client-vm printf '\x00\x00\x00\x3f'"$(echo '{"id":"1","method":"get_public_key","params":[{"nostr_index":0}]}')" | qrexec-client-vm qubes.NsignerRpc | tail -c +5 ``` Python (stdlib only, zero deps): ```python import json, struct, subprocess 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]) ``` See [`client/demo_python.py`](../client/demo_python.py) for a full working demo (get_public_key → sign_event → nip44 → mine_event). ### 3.2 Local Unix abstract socket (same machine) ```bash # Find running signers nsigner list # Connect via socat or the nsigner client subcommand nsigner client '' --socket-name ``` C (using `nostr_core_lib`): ```c nsigner_transport_t *t = nsigner_transport_open_unix("nsigner", 5000); nsigner_client_t *c = nsigner_client_new(t); 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) and [`examples/sign_event_client.c`](../examples/sign_event_client.c). ### 3.3 TCP (FIPS mesh, cross-qube) Requires a **kind-27235 auth envelope** (signed Nostr event proving caller identity). Python (with `coincurve`): ```python import hashlib, json, socket, struct, time from coincurve import PrivateKey # Build auth envelope sk = PrivateKey(caller_privkey_bytes) pubkey_x = sk.public_key.format(compressed=False)[1:33].hex() body_hash = hashlib.sha256(json.dumps(params, separators=(",",":")).encode()).hexdigest() tags = [["nsigner_rpc","1"],["nsigner_method","get_public_key"],["nsigner_body_hash",body_hash]] serialized = json.dumps([0, pubkey_x, created_at, 27235, tags, content], 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":"py-min","sig":sig}} ``` 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). ### 3.4 HTTP listener ```bash curl -X POST http://:/ \ -H "Content-Type: application/json" \ -d '{"id":"1","method":"get_public_key","params":[],"auth":{...}}' ``` ### 3.5 USB/serial (hardware signers) For Feather S3, Teensy 4.1, CYD ESP32, etc. — connect over USB CDC serial with the same framing. See [`examples/feather_get_public_key.py`](../examples/feather_get_public_key.py) and [`examples/feather_sign_event.py`](../examples/feather_sign_event.py). ## 4. Key verbs | Verb | What it does | Params | |------|-------------|--------| | `get_public_key` | Get pubkey for algorithm+index | `[{"algorithm":"secp256k1","index":0}]` | | `nostr_get_public_key` | Get secp256k1 pubkey by nostr_index | `[{"nostr_index":0}]` | | `nostr_sign_event` | Sign a Nostr event | `["",{"nostr_index":0}]` | | `sign` | Sign arbitrary bytes (any algorithm) | `["",{"algorithm":"ed25519","index":0}]` | | `nostr_nip44_encrypt` | NIP-44 encrypt | `["","",{"nostr_index":0}]` | | `nostr_nip44_decrypt` | NIP-44 decrypt | `["<peer_hex>","<ciphertext>",{"nostr_index":0}]` | | `nostr_mine_event` | NIP-13 PoW mine + sign | `["<event_json>",{"nostr_index":0,"difficulty":4}]` | | `encapsulate` | ML-KEM-768 encapsulate | `["<peer_pubkey_hex>",{"algorithm":"ml-kem-768"}]` | | `decapsulate` | ML-KEM-768 decapsulate | `["<ciphertext_hex>",{"algorithm":"ml-kem-768","index":0}]` | | `derive` | HMAC-SHA256(privkey, data) | `["<data>",{"algorithm":"secp256k1","index":0}]` | | `get_info` | Signer metadata | `[]` | Full verb table: [`README.md §4.3`](../README.md#43-verbs) ## 5. Algorithms | Algorithm | Key type | FIPS | Derivation path | |-----------|----------|------|-----------------| | `secp256k1` | Signature (Nostr) | — | `m/44'/1237'/<n>'/0/0` | | `ed25519` | Signature (SSH) | — | `m/44'/102001'/<n>'/0/0'` | | `x25519` | Key agreement (age) | — | `m/44'/102002'/<n>'/0/0'` | | `ml-dsa-65` | PQ signature | FIPS 204 | DRBG from seed | | `slh-dsa-128s` | PQ hash-based sig | FIPS 205 | DRBG from seed | | `ml-kem-768` | PQ KEM | FIPS 203 | DRBG from seed | | `otp` | One-time pad | — | USB pad, no derivation | ## 6. Publishing a Nostr event (end-to-end) ```python import json, struct, subprocess, 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]) # 1. Get pubkey pub = call_nsigner("nostr_signer", {"id":"1","method":"get_public_key","params":[{"nostr_index":0}]})["result"] # 2. Build and sign event event = {"kind":1,"content":"Hello from my agent!","created_at":int(time.time()),"tags":[],"pubkey":pub} result = call_nsigner("nostr_signer", {"id":"2","method":"nostr_sign_event","params":[json.dumps(event),{"nostr_index":0}]}) signed = json.loads(result["result"]) # 3. Broadcast to relay(s) # signed["id"] and signed["sig"] are now populated ``` ## 7. Reference files | File | What it shows | |------|---------------| | [`client/demo_python.py`](../client/demo_python.py) | Full Python demo (qrexec, stdlib only) | | [`client/demo_javascript.js`](../client/demo_javascript.js) | Full Node.js demo (qrexec) | | [`client/demo_c99.c`](../client/demo_c99.c) | Full C99 demo (qrexec, nostr_core_lib) | | [`examples/get_pubkey_fips.py`](../examples/get_pubkey_fips.py) | Minimal TCP/FIPS with auth envelope | | [`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 | | [`examples/n_signer_qube_example_qrexec.js`](../examples/n_signer_qube_example_qrexec.js) | qrexec in Node.js | | [`documents/CLIENT_IMPLEMENTATION.md`](../documents/CLIENT_IMPLEMENTATION.md) | Full wire contract spec | | [`documents/AGENT_CLIENT.md`](../documents/AGENT_CLIENT.md) | Comprehensive agent reference |