156 lines
4.8 KiB
JavaScript
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);
|
|
});
|