v0.0.34 - Add qrexec bridge subcommand and --bridge-source-trusted flag for persistent-signer qrexec transport
This commit is contained in:
Binary file not shown.
195
examples/get_pubkey_tcp.c
Normal file
195
examples/get_pubkey_tcp.c
Normal file
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* get_pubkey_tcp.c — connect to a running n_signer over TCP 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:[::]:8080, and this client runs in
|
||||
* a different qube connecting to the signer's FIPS address.
|
||||
*
|
||||
* Usage:
|
||||
* ./get_pubkey_tcp <host> <port>
|
||||
* ./get_pubkey_tcp npub1xxx...fips 8080
|
||||
*
|
||||
* If no arguments are given, defaults to localhost:8080.
|
||||
*
|
||||
* Output: for each index, prints:
|
||||
* index 0: hex=<64 hex chars> npub=npub1...
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "nsigner_transport.h"
|
||||
#include "nsigner_client.h"
|
||||
#include "nip019.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
/* Demo caller key (32 bytes). Replace with your stable caller key in real use. */
|
||||
static const unsigned char DEMO_PRIVKEY[32] = {
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
|
||||
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32
|
||||
};
|
||||
|
||||
static int hex_to_bytes(const char *hex, unsigned char *out, size_t out_len) {
|
||||
size_t len = strlen(hex);
|
||||
if (len != out_len * 2) {
|
||||
return -1;
|
||||
}
|
||||
for (size_t i = 0; i < out_len; i++) {
|
||||
unsigned int byte;
|
||||
if (sscanf(hex + 2 * i, "%2x", &byte) != 1) {
|
||||
return -1;
|
||||
}
|
||||
out[i] = (unsigned char)byte;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int query_pubkey(const char *host, int port, int nostr_index,
|
||||
char *out_hex, size_t hex_size, char *out_npub, size_t npub_size) {
|
||||
(void)npub_size; /* npub buffer size is enforced by nostr_key_to_bech32 output */
|
||||
nsigner_transport_t *transport = NULL;
|
||||
nsigner_client_t *client = NULL;
|
||||
cJSON *params = NULL;
|
||||
cJSON *opts = NULL;
|
||||
cJSON *result = NULL;
|
||||
const char *hex_pubkey = NULL;
|
||||
unsigned char pubkey_bytes[32];
|
||||
int rc = -1;
|
||||
|
||||
transport = nsigner_transport_open_tcp(host, port, 10000);
|
||||
if (transport == NULL) {
|
||||
fprintf(stderr, "connect failed: cannot open TCP transport to %s:%d\n", host, port);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
client = nsigner_client_new(transport);
|
||||
if (client == NULL) {
|
||||
fprintf(stderr, "connect failed: cannot create nsigner client\n");
|
||||
transport->close(transport);
|
||||
goto cleanup;
|
||||
}
|
||||
transport = NULL; /* owned by client now */
|
||||
|
||||
/* Set auth envelope — required for TCP listeners */
|
||||
if (nsigner_client_set_auth(client, DEMO_PRIVKEY, "get_pubkey_tcp") != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "failed to set auth envelope\n");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
/* params: [{"nostr_index": N}] */
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) {
|
||||
fprintf(stderr, "out of memory\n");
|
||||
goto cleanup;
|
||||
}
|
||||
opts = cJSON_CreateObject();
|
||||
if (opts == NULL) {
|
||||
fprintf(stderr, "out of memory\n");
|
||||
goto cleanup;
|
||||
}
|
||||
cJSON_AddNumberToObject(opts, "nostr_index", nostr_index);
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
opts = NULL;
|
||||
|
||||
if (nsigner_client_call(client, "get_public_key", params, &result) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "request failed for index %d: %s\n", nostr_index,
|
||||
nsigner_client_last_error(client));
|
||||
params = NULL; /* nsigner_client_call took ownership even on failure */
|
||||
goto cleanup;
|
||||
}
|
||||
params = NULL; /* nsigner_client_call took ownership */
|
||||
|
||||
if (!cJSON_IsString(result)) {
|
||||
fprintf(stderr, "index %d: unexpected result type\n", nostr_index);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
hex_pubkey = result->valuestring;
|
||||
if (strlen(hex_pubkey) != 64) {
|
||||
fprintf(stderr, "index %d: unexpected pubkey length: %zu\n", nostr_index, strlen(hex_pubkey));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
strncpy(out_hex, hex_pubkey, hex_size - 1);
|
||||
out_hex[hex_size - 1] = '\0';
|
||||
|
||||
/* Convert hex pubkey to npub (bech32) */
|
||||
if (hex_to_bytes(hex_pubkey, pubkey_bytes, 32) != 0) {
|
||||
fprintf(stderr, "index %d: failed to parse hex pubkey\n", nostr_index);
|
||||
goto cleanup;
|
||||
}
|
||||
if (nostr_key_to_bech32(pubkey_bytes, "npub", out_npub) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "index %d: failed to convert to npub\n", nostr_index);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
rc = 0;
|
||||
|
||||
cleanup:
|
||||
cJSON_Delete(opts);
|
||||
cJSON_Delete(params);
|
||||
cJSON_Delete(result);
|
||||
nsigner_client_free(client);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *host = "127.0.0.1";
|
||||
int port = 8080;
|
||||
char hex0[65], npub0[128];
|
||||
char hex1[65], npub1[128];
|
||||
int failures = 0;
|
||||
|
||||
if (argc > 1) {
|
||||
host = argv[1];
|
||||
}
|
||||
if (argc > 2) {
|
||||
port = atoi(argv[2]);
|
||||
if (port <= 0 || port > 65535) {
|
||||
fprintf(stderr, "Invalid port: %s\n", argv[2]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "failed to initialize crypto subsystem\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("Connecting to n_signer at %s:%d\n", host, port);
|
||||
printf("Querying get_public_key for nostr_index 0 and 1...\n\n");
|
||||
|
||||
/* Query index 0 */
|
||||
hex0[0] = '\0';
|
||||
npub0[0] = '\0';
|
||||
if (query_pubkey(host, port, 0, hex0, sizeof(hex0), npub0, sizeof(npub0)) == 0) {
|
||||
printf("index 0: hex=%s npub=%s\n", hex0, npub0);
|
||||
} else {
|
||||
printf("index 0: FAILED\n");
|
||||
failures++;
|
||||
}
|
||||
|
||||
/* Query index 1 */
|
||||
hex1[0] = '\0';
|
||||
npub1[0] = '\0';
|
||||
if (query_pubkey(host, port, 1, hex1, sizeof(hex1), npub1, sizeof(npub1)) == 0) {
|
||||
printf("index 1: hex=%s npub=%s\n", hex1, npub1);
|
||||
} else {
|
||||
printf("index 1: FAILED\n");
|
||||
failures++;
|
||||
}
|
||||
|
||||
nostr_cleanup();
|
||||
|
||||
if (failures > 0) {
|
||||
printf("\n%d query(s) failed\n", failures);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("\nAll queries succeeded\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -1,32 +1,100 @@
|
||||
/*
|
||||
* get_public_key_client.c — connect to a running n_signer over its abstract
|
||||
* UNIX socket and call get_public_key, using the shared nsigner client from
|
||||
* nostr_core_lib (nostr_core/nsigner_client.h + nostr_core/nsigner_transport.h).
|
||||
*
|
||||
* Usage: ./get_public_key_client [socket_name]
|
||||
*
|
||||
* Output: the raw JSON-RPC response string, e.g.
|
||||
* {"id":"1","result":"<pubkey hex>"}
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "../client/nsigner_client.h"
|
||||
#include "nostr_common.h"
|
||||
#include "nsigner_transport.h"
|
||||
#include "nsigner_client.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *socket_name = "nsigner";
|
||||
nsigner_client_t client;
|
||||
char *response = NULL;
|
||||
nsigner_transport_t *transport = NULL;
|
||||
nsigner_client_t *client = NULL;
|
||||
cJSON *params = NULL;
|
||||
cJSON *result = NULL;
|
||||
cJSON *response = NULL;
|
||||
char *response_json = NULL;
|
||||
int rc = 1;
|
||||
|
||||
if (argc > 1 && argv[1] != NULL && argv[1][0] != '\0') {
|
||||
socket_name = argv[1];
|
||||
}
|
||||
|
||||
nsigner_client_init(&client);
|
||||
|
||||
if (nsigner_client_connect_unix(&client, socket_name, 5000) != 0) {
|
||||
fprintf(stderr, "connect failed: %s\n", nsigner_client_last_error(&client));
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "failed to initialize crypto subsystem\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (nsigner_client_get_public_key(&client, "example-1", "", &response) != 0) {
|
||||
fprintf(stderr, "request failed: %s\n", nsigner_client_last_error(&client));
|
||||
nsigner_client_close(&client);
|
||||
return 1;
|
||||
transport = nsigner_transport_open_unix(socket_name, 5000);
|
||||
if (transport == NULL) {
|
||||
fprintf(stderr, "connect failed: cannot open unix transport @%s\n", socket_name);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
printf("%s\n", response);
|
||||
free(response);
|
||||
nsigner_client_close(&client);
|
||||
return 0;
|
||||
client = nsigner_client_new(transport);
|
||||
if (client == NULL) {
|
||||
fprintf(stderr, "connect failed: cannot create nsigner client\n");
|
||||
/* nsigner_client_new takes ownership of transport on success only */
|
||||
transport->close(transport);
|
||||
goto cleanup;
|
||||
}
|
||||
transport = NULL; /* owned by client now */
|
||||
|
||||
/* params: [] (empty array — server picks the default role) */
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) {
|
||||
fprintf(stderr, "out of memory\n");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (nsigner_client_call(client, "get_public_key", params, &result) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "request failed: %s\n", nsigner_client_last_error(client));
|
||||
goto cleanup;
|
||||
}
|
||||
params = NULL; /* nsigner_client_call takes ownership of params */
|
||||
|
||||
/*
|
||||
* Reconstruct a JSON-RPC response string so the CLI output stays
|
||||
* backward-compatible with the old client example:
|
||||
* {"id":"<id>","result":"<pubkey hex>"}
|
||||
* nsigner_client_call returns only the parsed `result` element and does
|
||||
* not expose the server-assigned id, so we emit a minimal envelope.
|
||||
*/
|
||||
response = cJSON_CreateObject();
|
||||
if (response == NULL) {
|
||||
fprintf(stderr, "out of memory\n");
|
||||
goto cleanup;
|
||||
}
|
||||
cJSON_AddStringToObject(response, "id", "1");
|
||||
cJSON_AddItemReferenceToObject(response, "result", result);
|
||||
|
||||
response_json = cJSON_PrintUnformatted(response);
|
||||
if (response_json == NULL) {
|
||||
fprintf(stderr, "failed to serialize response\n");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
printf("%s\n", response_json);
|
||||
rc = 0;
|
||||
|
||||
cleanup:
|
||||
free(response_json);
|
||||
cJSON_Delete(response);
|
||||
cJSON_Delete(result);
|
||||
cJSON_Delete(params);
|
||||
nsigner_client_free(client); /* also closes/frees the transport */
|
||||
nostr_cleanup();
|
||||
return rc;
|
||||
}
|
||||
|
||||
257
examples/n_signer_qube_example_fips.js
Normal file
257
examples/n_signer_qube_example_fips.js
Normal file
@@ -0,0 +1,257 @@
|
||||
#!/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:[::]:8080, 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 8080
|
||||
*
|
||||
* If no arguments are given, defaults to localhost:8080.
|
||||
*
|
||||
* 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":"<pubkey hex>"} 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] || "8080", 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);
|
||||
});
|
||||
155
examples/n_signer_qube_example_qrexec.js
Normal file
155
examples/n_signer_qube_example_qrexec.js
Normal file
@@ -0,0 +1,155 @@
|
||||
#!/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);
|
||||
});
|
||||
@@ -1,33 +1,114 @@
|
||||
/*
|
||||
* sign_event_client.c — connect to a running n_signer over its abstract
|
||||
* UNIX socket and call sign_event, using the shared nsigner client from
|
||||
* nostr_core_lib (nostr_core/nsigner_client.h + nostr_core/nsigner_transport.h).
|
||||
*
|
||||
* Usage: ./sign_event_client [socket_name]
|
||||
*
|
||||
* Output: the raw JSON-RPC response string, e.g.
|
||||
* {"id":"1","result":"{...signed event...}"}
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "../client/nsigner_client.h"
|
||||
#include "nostr_common.h"
|
||||
#include "nsigner_transport.h"
|
||||
#include "nsigner_client.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *socket_name = "nsigner";
|
||||
const char *event_json = "{\"kind\":1,\"content\":\"hello from client example\",\"tags\":[],\"created_at\":1700000000}";
|
||||
nsigner_client_t client;
|
||||
char *response = NULL;
|
||||
const char *role = "main";
|
||||
nsigner_transport_t *transport = NULL;
|
||||
nsigner_client_t *client = NULL;
|
||||
cJSON *params = NULL;
|
||||
cJSON *opts = NULL;
|
||||
cJSON *result = NULL;
|
||||
cJSON *response = NULL;
|
||||
char *response_json = NULL;
|
||||
int rc = 1;
|
||||
|
||||
if (argc > 1 && argv[1] != NULL && argv[1][0] != '\0') {
|
||||
socket_name = argv[1];
|
||||
}
|
||||
|
||||
nsigner_client_init(&client);
|
||||
|
||||
if (nsigner_client_connect_unix(&client, socket_name, 5000) != 0) {
|
||||
fprintf(stderr, "connect failed: %s\n", nsigner_client_last_error(&client));
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "failed to initialize crypto subsystem\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (nsigner_client_sign_event(&client, "example-2", event_json, "main", &response) != 0) {
|
||||
fprintf(stderr, "request failed: %s\n", nsigner_client_last_error(&client));
|
||||
nsigner_client_close(&client);
|
||||
return 1;
|
||||
transport = nsigner_transport_open_unix(socket_name, 5000);
|
||||
if (transport == NULL) {
|
||||
fprintf(stderr, "connect failed: cannot open unix transport @%s\n", socket_name);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
printf("%s\n", response);
|
||||
free(response);
|
||||
nsigner_client_close(&client);
|
||||
return 0;
|
||||
client = nsigner_client_new(transport);
|
||||
if (client == NULL) {
|
||||
fprintf(stderr, "connect failed: cannot create nsigner client\n");
|
||||
transport->close(transport);
|
||||
goto cleanup;
|
||||
}
|
||||
transport = NULL; /* owned by client now */
|
||||
|
||||
/* params: [ "<event_json>", {"role":"main"} ] */
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) {
|
||||
fprintf(stderr, "out of memory\n");
|
||||
goto cleanup;
|
||||
}
|
||||
cJSON_AddItemToArray(params, cJSON_CreateString(event_json));
|
||||
|
||||
opts = cJSON_CreateObject();
|
||||
if (opts == NULL) {
|
||||
fprintf(stderr, "out of memory\n");
|
||||
goto cleanup;
|
||||
}
|
||||
if (role != NULL && role[0] != '\0') {
|
||||
cJSON_AddStringToObject(opts, "role", role);
|
||||
}
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
opts = NULL; /* owned by params now */
|
||||
|
||||
if (nsigner_client_call(client, "sign_event", params, &result) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "request failed: %s\n", nsigner_client_last_error(client));
|
||||
goto cleanup;
|
||||
}
|
||||
params = NULL; /* nsigner_client_call takes ownership of params */
|
||||
|
||||
/*
|
||||
* Reconstruct a JSON-RPC response string for backward-compatible CLI
|
||||
* output: {"id":"1","result":"<signed event json>"}
|
||||
* The server returns the signed event as a JSON string (not an object),
|
||||
* so result is a cJSON string here.
|
||||
*/
|
||||
response = cJSON_CreateObject();
|
||||
if (response == NULL) {
|
||||
fprintf(stderr, "out of memory\n");
|
||||
goto cleanup;
|
||||
}
|
||||
cJSON_AddStringToObject(response, "id", "1");
|
||||
cJSON_AddItemReferenceToObject(response, "result", result);
|
||||
|
||||
response_json = cJSON_PrintUnformatted(response);
|
||||
if (response_json == NULL) {
|
||||
fprintf(stderr, "failed to serialize response\n");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
printf("%s\n", response_json);
|
||||
rc = 0;
|
||||
|
||||
cleanup:
|
||||
free(response_json);
|
||||
cJSON_Delete(response);
|
||||
cJSON_Delete(result);
|
||||
cJSON_Delete(params);
|
||||
cJSON_Delete(opts);
|
||||
nsigner_client_free(client); /* also closes/frees the transport */
|
||||
nostr_cleanup();
|
||||
return rc;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user