Files
n_signer/examples/n_signer_qube_example_qrexec.js

156 lines
4.8 KiB
JavaScript

#!/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:<source-vm>)
*/
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:<source-vm> 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);
});