#!/usr/bin/env node /** * n_signer_qube_example.js — connect to a running n_signer over TCP (FIPS mesh) * and call get_public_key for nostr_index 0 and 1, printing both hex pubkey * and bech32 npub for each. * * This is a cross-qube test client for Qubes OS: the signer runs in the * nostr_signer qube listening on tcp:[::]:11111, and this client runs in * a different qube connecting to the signer's FIPS address. * * Usage: * node n_signer_qube_example.js [host] [port] * node n_signer_qube_example.js fd56:d7c3:f605:719d:15b:18a0:fb06:982f 11111 * * If no arguments are given, defaults to localhost:11111. * * Protocol: * - 4-byte big-endian length prefix + JSON payload (TCP framing) * - JSON-RPC: {"id":"...","method":"get_public_key","params":[{"nostr_index":N}],"auth":{...}} * - Auth envelope: kind 27235 Nostr event with nsigner_rpc, nsigner_method, * nsigner_body_hash tags. body_hash = sha256(json.dumps(params)) * - Response: {"id":"...","result":""} or {"id":"...","error":{...}} */ const net = require("net"); const crypto = require("crypto"); const secp = require("@noble/secp256k1"); // @noble/secp256k1 v3 requires us to provide sync sha256/hmacSha256 secp.hashes.sha256 = (msg) => new Uint8Array(crypto.createHash("sha256").update(msg).digest()); secp.hashes.hmacSha256 = (key, msg) => new Uint8Array(crypto.createHmac("sha256", key).update(msg).digest()); const { schnorr } = secp; /** Convert a Uint8Array to a hex string. */ function bytesToHex(bytes) { return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); } /** Convert a hex string to a Uint8Array. */ function hexToBytes(hex) { const arr = new Uint8Array(hex.length / 2); for (let i = 0; i < arr.length; i++) { arr[i] = parseInt(hex.substr(i * 2, 2), 16); } return arr; } // Demo caller key (32 bytes). Replace with your stable caller key in real use. const DEMO_PRIVKEY = new Uint8Array(Array.from({ length: 32 }, (_, i) => i + 1)); /** * Compute the body hash for a params array. * This must match n_signer's hashing: sha256 of canonical JSON (no spaces). */ function computeBodyHash(params) { const canonical = JSON.stringify(params); return crypto.createHash("sha256").update(canonical).digest("hex"); } /** * Build a kind-27235 auth envelope for the given method and params. * Returns the auth object to include in the JSON-RPC request. */ function buildAuthEnvelope(privkey, method, params, requestId) { const bodyHash = computeBodyHash(params); const pubkeyHex = bytesToHex(schnorr.getPublicKey(privkey)); const createdAt = Math.floor(Date.now() / 1000); // nsigner_rpc tag value MUST match the JSON-RPC request "id" field const tags = [ ["nsigner_rpc", requestId], ["nsigner_method", method], ["nsigner_body_hash", bodyHash], ]; const content = "js-example"; // Serialize the event for signing: [0, pubkey, created_at, kind, tags, content] const serialized = JSON.stringify([ 0, pubkeyHex, createdAt, 27235, tags, content, ]); const eventId = crypto.createHash("sha256").update(serialized).digest("hex"); const sig = bytesToHex(schnorr.sign(hexToBytes(eventId), privkey, new Uint8Array(32))); return { id: eventId, pubkey: pubkeyHex, created_at: createdAt, kind: 27235, tags: tags, content: content, sig: sig, }; } /** * Send a framed JSON-RPC request over a TCP socket and receive the response. * Framing: 4-byte big-endian length prefix + JSON payload. */ function sendRequest(socket, request) { return new Promise((resolve, reject) => { const payload = Buffer.from(JSON.stringify(request), "utf8"); const header = Buffer.alloc(4); header.writeUInt32BE(payload.length, 0); socket.write(Buffer.concat([header, payload])); let headerBuf = Buffer.alloc(0); let bodyBuf = Buffer.alloc(0); let bodyLen = 0; let state = "header"; const onData = (chunk) => { if (state === "header") { headerBuf = Buffer.concat([headerBuf, chunk]); if (headerBuf.length >= 4) { bodyLen = headerBuf.readUInt32BE(0); const remaining = headerBuf.subarray(4); headerBuf = Buffer.alloc(0); state = "body"; if (remaining.length > 0) { bodyBuf = Buffer.concat([bodyBuf, remaining]); } if (bodyBuf.length >= bodyLen) { finish(); } } } else if (state === "body") { bodyBuf = Buffer.concat([bodyBuf, chunk]); if (bodyBuf.length >= bodyLen) { finish(); } } }; function finish() { socket.off("data", onData); socket.off("error", onError); const body = bodyBuf.subarray(0, bodyLen).toString("utf8"); try { resolve(JSON.parse(body)); } catch (e) { reject(new Error(`Failed to parse response: ${e.message}`)); } } function onError(err) { socket.off("data", onData); reject(err); } socket.on("data", onData); socket.on("error", onError); }); } /** * Query get_public_key for a given nostr_index. * Opens a fresh TCP connection for each request (n_signer handles one request * per connection). */ async function getPublicKey(host, port, nostrIndex, privkey) { const params = [{ nostr_index: nostrIndex }]; const requestId = String(nostrIndex); const auth = buildAuthEnvelope(privkey, "get_public_key", params, requestId); const request = { id: requestId, method: "get_public_key", params: params, auth: auth, }; return new Promise((resolve, reject) => { const socket = new net.Socket(); socket.setTimeout(15000); socket.connect(port, host, async () => { try { const response = await sendRequest(socket, request); socket.destroy(); resolve(response); } catch (e) { socket.destroy(); reject(e); } }); socket.on("timeout", () => { socket.destroy(); reject(new Error("Connection timed out")); }); socket.on("error", (err) => { reject(err); }); }); } /** * Convert a 32-byte hex pubkey to bech32 npub format (NIP-19). */ function hexToNpub(pubkeyHex) { const { nip19 } = require("nostr-tools"); return nip19.npubEncode(pubkeyHex); } async function main() { const host = process.argv[2] || "127.0.0.1"; const port = parseInt(process.argv[3] || "11111", 10); console.log(`Connecting to n_signer at ${host}:${port}`); console.log("Querying get_public_key for nostr_index 0 and 1...\n"); let failures = 0; for (const index of [0, 1]) { // Small delay between requests to avoid auth nonce collision // (the auth envelope uses created_at as part of the nonce) if (index > 0) await new Promise((r) => setTimeout(r, 1100)); try { const response = await getPublicKey(host, port, index, DEMO_PRIVKEY); if (response.error) { console.log(`index ${index}: ERROR: ${JSON.stringify(response.error)}`); failures++; continue; } const pubkeyHex = response.result; const npub = hexToNpub(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); });