#!/usr/bin/env node /** * n_signer_qube_example_qrexec.js — connect to n_signer in the nostr_signer * qube via Qubes qrexec (qubes.NsignerRpc service) and call get_public_key * for nostr_index 0 and 1, printing both hex pubkey and bech32 npub. * * This uses Qubes OS's built-in secure IPC (qrexec) instead of FIPS TCP. * No network connectivity is required — all traffic stays within the host. * * Prerequisites: * - The qubes.NsignerRpc service must be installed in the nostr_signer qube * (see packaging/qubes/install-service.sh) * - The qrexec policy must be installed in dom0 * (see packaging/qubes/install-policy.sh) * - The nostr_signer qube must be tagged with 'nsigner-signer' * (qvm-tags nostr_signer add nsigner-signer) * - A mnemonic file must exist at /home/user/.nsigner_mnemonic in the * nostr_signer qube * * Usage: * node n_signer_qube_example_qrexec.js [target_qube] * node n_signer_qube_example_qrexec.js nostr_signer * * If no argument is given, defaults to "nostr_signer". * * Protocol: * - qrexec-client-vm spawns the qubes.NsignerRpc service in the target qube * - The service runs: nsigner --listen qrexec --allow-all --mnemonic-fd 3 * - We send one framed JSON-RPC request via stdin, receive one framed response via stdout * - Framing: 4-byte big-endian length prefix + JSON payload * - Auth: not required in qrexec mode (caller identity is qubes:) */ const { spawn } = require("child_process"); const { nip19 } = require("nostr-tools"); /** * Frame a JSON-RPC request: 4-byte big-endian length + JSON payload. */ function frameRequest(obj) { const payload = Buffer.from(JSON.stringify(obj), "utf8"); const header = Buffer.alloc(4); header.writeUInt32BE(payload.length, 0); return Buffer.concat([header, payload]); } /** * Parse a framed response from the qrexec stdout buffer. */ function parseFramedResponse(buf) { if (buf.length < 4) { throw new Error("short response (missing frame header)"); } const len = buf.readUInt32BE(0); const payload = buf.subarray(4, 4 + len); if (payload.length !== len) { throw new Error(`short response payload: expected ${len}, got ${payload.length}`); } return JSON.parse(payload.toString("utf8")); } /** * Call nsigner via qrexec. Sends one framed request, receives one framed response. * Each call spawns a fresh qrexec-client-vm process (one request per invocation). */ function callNsignerQrexec(targetQube, request) { return new Promise((resolve, reject) => { const framed = frameRequest(request); const proc = spawn("qrexec-client-vm", [targetQube, "qubes.NsignerRpc"], { stdio: ["pipe", "pipe", "pipe"], }); const stdoutChunks = []; const stderrChunks = []; proc.stdout.on("data", (chunk) => stdoutChunks.push(chunk)); proc.stderr.on("data", (chunk) => stderrChunks.push(chunk)); proc.on("error", (err) => { reject(new Error(`failed to spawn qrexec-client-vm: ${err.message}`)); }); proc.on("close", (code) => { if (code !== 0) { const stderr = Buffer.concat(stderrChunks).toString("utf8"); reject(new Error(`qrexec-client-vm exited with code ${code}: ${stderr.trim()}`)); return; } try { const response = parseFramedResponse(Buffer.concat(stdoutChunks)); resolve(response); } catch (e) { reject(new Error(`failed to parse response: ${e.message}`)); } }); // Send the framed request and close stdin proc.stdin.write(framed); proc.stdin.end(); }); } /** * Query get_public_key for a given nostr_index via qrexec. * No auth envelope needed — qrexec mode uses qubes: as caller identity. */ async function getPublicKey(targetQube, nostrIndex) { const request = { id: String(nostrIndex), method: "get_public_key", params: [{ nostr_index: nostrIndex }], }; return callNsignerQrexec(targetQube, request); } async function main() { const targetQube = process.argv[2] || "nostr_signer"; console.log(`Calling n_signer in qube "${targetQube}" via qrexec...`); console.log("Querying get_public_key for nostr_index 0 and 1...\n"); let failures = 0; for (const index of [0, 1]) { try { const response = await getPublicKey(targetQube, index); if (response.error) { console.log(`index ${index}: ERROR: ${JSON.stringify(response.error)}`); failures++; continue; } const pubkeyHex = response.result; const npub = nip19.npubEncode(pubkeyHex); console.log(`index ${index}: hex=${pubkeyHex} npub=${npub}`); } catch (e) { console.log(`index ${index}: FAILED - ${e.message}`); failures++; } } if (failures > 0) { console.log(`\n${failures} query(s) failed`); process.exit(1); } console.log("\nAll queries succeeded"); } main().catch((e) => { console.error(e); process.exit(1); });