323 lines
9.9 KiB
C
323 lines
9.9 KiB
C
/*
|
|
* demo_c99.c — comprehensive C99 demo for connecting to a running n_signer
|
|
* via Qubes qrexec and performing all three core operations:
|
|
*
|
|
* 1. get_public_key — retrieve a Nostr public key by nostr_index
|
|
* 2. sign_event — sign a Nostr event (kind 1 text note)
|
|
* 3. nip44_encrypt — encrypt a message to a peer (and decrypt it back)
|
|
*
|
|
* Note: mine_event (NIP-13 PoW) is also available via the JSON-RPC interface.
|
|
* See demo_javascript.js and demo_python.py for mine_event usage examples.
|
|
* The high-level nostr_signer API does not yet wrap mine_event.
|
|
*
|
|
* This uses the high-level nostr_signer API from nostr_core_lib:
|
|
* - nostr_signer_nsigner_qrexec() — qrexec transport (no network)
|
|
* - nostr_signer_nsigner_set_nostr_index() — select key by NIP-06 index
|
|
* - nostr_signer_get_public_key() — get pubkey
|
|
* - nostr_signer_sign_event() — sign an event
|
|
* - nostr_signer_nip44_encrypt() — encrypt
|
|
* - nostr_signer_nip44_decrypt() — decrypt
|
|
*
|
|
* Prerequisites:
|
|
* - n_signer running in the target qube with --bridge-source-trusted
|
|
* - qubes.NsignerRpc service installed in the target qube
|
|
* - dom0 qrexec policy allowing this qube to call the service
|
|
*
|
|
* Build (from n_signer repo root):
|
|
* make examples
|
|
*
|
|
* Usage:
|
|
* ./build/demo_c99 <target_qube> [nostr_index]
|
|
* ./build/demo_c99 nostr_signer 1
|
|
*
|
|
* If no nostr_index is given, defaults to 0.
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <signal.h>
|
|
|
|
#include "nostr_common.h"
|
|
#include "nostr_signer.h"
|
|
#include "nip019.h"
|
|
#include "../cjson/cJSON.h"
|
|
|
|
/* Helper: convert 32-byte hex pubkey to bech32 npub */
|
|
static int hex_to_npub(const char *hex, char *out_npub, size_t out_sz) {
|
|
unsigned char bytes[32];
|
|
int i;
|
|
|
|
if (strlen(hex) != 64) {
|
|
return -1;
|
|
}
|
|
|
|
for (i = 0; i < 32; i++) {
|
|
unsigned int byte;
|
|
if (sscanf(hex + 2 * i, "%2x", &byte) != 1) {
|
|
return -1;
|
|
}
|
|
bytes[i] = (unsigned char)byte;
|
|
}
|
|
|
|
return nostr_key_to_bech32(bytes, "npub", out_npub);
|
|
}
|
|
|
|
/* Helper: print an error with a human-readable description */
|
|
static void print_error(const char *operation, int rc) {
|
|
const char *desc = "unknown error";
|
|
switch (rc) {
|
|
case NOSTR_ERROR_INVALID_INPUT:
|
|
desc = "invalid input";
|
|
break;
|
|
case NOSTR_ERROR_CRYPTO_FAILED:
|
|
desc = "crypto operation failed";
|
|
break;
|
|
case NOSTR_ERROR_IO_FAILED:
|
|
desc = "I/O failed (transport error)";
|
|
break;
|
|
case NOSTR_ERROR_NETWORK_FAILED:
|
|
desc = "network failed";
|
|
break;
|
|
case NOSTR_ERROR_NSIGNER_POLICY_DENIED:
|
|
desc = "policy denied (caller not approved at signer terminal)";
|
|
break;
|
|
case NOSTR_ERROR_NSIGNER_INDEX_NOT_ALLOWED:
|
|
desc = "index not in signer's whitelist";
|
|
break;
|
|
default:
|
|
/* Try to print the numeric code */
|
|
fprintf(stderr, " %s failed: error code %d\n", operation, rc);
|
|
return;
|
|
}
|
|
fprintf(stderr, " %s failed: %s (code %d)\n", operation, desc, rc);
|
|
}
|
|
|
|
/*
|
|
* Demo 1: Get a public key by nostr_index.
|
|
* Returns the hex pubkey in `out_hex` (must be 65 bytes).
|
|
*/
|
|
static int demo_get_public_key(nostr_signer_t *signer, int nostr_index,
|
|
char *out_hex, size_t hex_sz) {
|
|
char npub[128];
|
|
int rc;
|
|
|
|
printf("\n=== Demo 1: get_public_key (nostr_index=%d) ===\n", nostr_index);
|
|
|
|
rc = nostr_signer_get_public_key(signer, out_hex);
|
|
if (rc != NOSTR_SUCCESS) {
|
|
print_error("get_public_key", rc);
|
|
return rc;
|
|
}
|
|
|
|
if (hex_to_npub(out_hex, npub, sizeof(npub)) == 0) {
|
|
printf(" pubkey hex: %s\n", out_hex);
|
|
printf(" npub: %s\n", npub);
|
|
} else {
|
|
printf(" pubkey hex: %s\n", out_hex);
|
|
printf(" (npub conversion failed)\n");
|
|
}
|
|
|
|
return NOSTR_SUCCESS;
|
|
}
|
|
|
|
/*
|
|
* Demo 2: Sign a Nostr event (kind 1 text note).
|
|
* The signed event JSON is printed.
|
|
*/
|
|
static int demo_sign_event(nostr_signer_t *signer, const char *pubkey_hex) {
|
|
cJSON *unsigned_event = NULL;
|
|
cJSON *signed_event = NULL;
|
|
char *signed_json = NULL;
|
|
int rc;
|
|
|
|
printf("\n=== Demo 2: sign_event (kind 1 text note) ===\n");
|
|
|
|
/* Build an unsigned Nostr event (kind 1 text note) */
|
|
unsigned_event = cJSON_CreateObject();
|
|
if (unsigned_event == NULL) {
|
|
fprintf(stderr, " failed to create event JSON\n");
|
|
return NOSTR_ERROR_MEMORY_FAILED;
|
|
}
|
|
|
|
cJSON_AddNumberToObject(unsigned_event, "kind", 1);
|
|
cJSON_AddStringToObject(unsigned_event, "content", "Hello from n_signer C99 demo!");
|
|
cJSON_AddNumberToObject(unsigned_event, "created_at", (int)time(NULL));
|
|
|
|
/* tags: empty array */
|
|
cJSON_AddItemToObject(unsigned_event, "tags", cJSON_CreateArray());
|
|
|
|
/* pubkey: the signer will fill this in, but we include it for completeness */
|
|
cJSON_AddStringToObject(unsigned_event, "pubkey", pubkey_hex);
|
|
|
|
printf(" Unsigned event:\n");
|
|
{
|
|
char *tmp = cJSON_PrintUnformatted(unsigned_event);
|
|
if (tmp) {
|
|
printf(" %s\n", tmp);
|
|
free(tmp);
|
|
}
|
|
}
|
|
|
|
/* Sign it */
|
|
rc = nostr_signer_sign_event(signer, unsigned_event, &signed_event);
|
|
if (rc != NOSTR_SUCCESS) {
|
|
print_error("sign_event", rc);
|
|
cJSON_Delete(unsigned_event);
|
|
return rc;
|
|
}
|
|
|
|
/* Print the signed event */
|
|
signed_json = cJSON_Print(signed_event);
|
|
if (signed_json) {
|
|
printf(" Signed event:\n");
|
|
printf(" %s\n", signed_json);
|
|
free(signed_json);
|
|
}
|
|
|
|
/* Extract and show the signature and event id */
|
|
{
|
|
cJSON *id = cJSON_GetObjectItemCaseSensitive(signed_event, "id");
|
|
cJSON *sig = cJSON_GetObjectItemCaseSensitive(signed_event, "sig");
|
|
if (id && cJSON_IsString(id)) {
|
|
printf(" event id: %s\n", id->valuestring);
|
|
}
|
|
if (sig && cJSON_IsString(sig)) {
|
|
printf(" signature: %s\n", sig->valuestring);
|
|
}
|
|
}
|
|
|
|
cJSON_Delete(signed_event);
|
|
cJSON_Delete(unsigned_event);
|
|
return NOSTR_SUCCESS;
|
|
}
|
|
|
|
/*
|
|
* Demo 3: NIP-44 encrypt and decrypt.
|
|
* Encrypts a message to ourselves (using our own pubkey as the peer),
|
|
* then decrypts it to verify round-trip.
|
|
*/
|
|
static int demo_nip44(nostr_signer_t *signer, const char *pubkey_hex) {
|
|
const char *plaintext = "Secret message from n_signer C99 demo!";
|
|
char *ciphertext = NULL;
|
|
char *decrypted = NULL;
|
|
int rc;
|
|
|
|
printf("\n=== Demo 3: nip44_encrypt / nip44_decrypt ===\n");
|
|
printf(" plaintext: \"%s\"\n", plaintext);
|
|
printf(" peer pubkey: %s (self)\n", pubkey_hex);
|
|
|
|
/* Encrypt */
|
|
rc = nostr_signer_nip44_encrypt(signer, pubkey_hex, plaintext, &ciphertext);
|
|
if (rc != NOSTR_SUCCESS) {
|
|
print_error("nip44_encrypt", rc);
|
|
return rc;
|
|
}
|
|
|
|
printf(" ciphertext: %s\n", ciphertext);
|
|
|
|
/* Decrypt (using our own pubkey as the sender) */
|
|
rc = nostr_signer_nip44_decrypt(signer, pubkey_hex, ciphertext, &decrypted);
|
|
if (rc != NOSTR_SUCCESS) {
|
|
print_error("nip44_decrypt", rc);
|
|
free(ciphertext);
|
|
return rc;
|
|
}
|
|
|
|
printf(" decrypted: \"%s\"\n", decrypted);
|
|
|
|
/* Verify round-trip */
|
|
if (strcmp(plaintext, decrypted) == 0) {
|
|
printf(" ✓ Round-trip verified: plaintext matches decrypted\n");
|
|
} else {
|
|
printf(" ✗ Round-trip FAILED: plaintext does not match decrypted\n");
|
|
rc = NOSTR_ERROR_CRYPTO_FAILED;
|
|
}
|
|
|
|
free(ciphertext);
|
|
free(decrypted);
|
|
return rc;
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
const char *target_qube;
|
|
const char *service_name = "qubes.NsignerRpc";
|
|
int nostr_index = 0;
|
|
nostr_signer_t *signer = NULL;
|
|
char pubkey_hex[65];
|
|
int rc;
|
|
|
|
/* Ignore SIGPIPE — qrexec subprocess may close pipes abruptly */
|
|
(void)signal(SIGPIPE, SIG_IGN);
|
|
|
|
if (argc < 2) {
|
|
fprintf(stderr, "Usage: %s <target_qube> [nostr_index]\n", argv[0]);
|
|
fprintf(stderr, "Example: %s nostr_signer 1\n", argv[0]);
|
|
return 1;
|
|
}
|
|
target_qube = argv[1];
|
|
if (argc > 2) {
|
|
nostr_index = atoi(argv[2]);
|
|
}
|
|
|
|
/* Initialize the crypto subsystem */
|
|
if (nostr_init() != NOSTR_SUCCESS) {
|
|
fprintf(stderr, "Failed to initialize crypto subsystem\n");
|
|
return 1;
|
|
}
|
|
|
|
printf("=== n_signer C99 Demo ===\n");
|
|
printf("Target qube: %s\n", target_qube);
|
|
printf("Service: %s\n", service_name);
|
|
printf("nostr_index: %d\n", nostr_index);
|
|
printf("\n");
|
|
|
|
/* Create a high-level signer backed by qrexec transport */
|
|
printf("Connecting to n_signer via qrexec...\n");
|
|
signer = nostr_signer_nsigner_qrexec(target_qube, service_name, NULL, 30000);
|
|
if (signer == NULL) {
|
|
fprintf(stderr, "Failed to create qrexec signer.\n");
|
|
fprintf(stderr, "Is qrexec-client-vm available? Is the service installed?\n");
|
|
nostr_cleanup();
|
|
return 1;
|
|
}
|
|
printf("Connected.\n");
|
|
|
|
/* Select key by nostr_index (NIP-06 m/44'/1237'/N'/0/0) */
|
|
rc = nostr_signer_nsigner_set_nostr_index(signer, nostr_index);
|
|
if (rc != NOSTR_SUCCESS) {
|
|
print_error("set_nostr_index", rc);
|
|
nostr_signer_free(signer);
|
|
nostr_cleanup();
|
|
return 1;
|
|
}
|
|
|
|
/* Demo 1: Get public key */
|
|
rc = demo_get_public_key(signer, nostr_index, pubkey_hex, sizeof(pubkey_hex));
|
|
if (rc != NOSTR_SUCCESS) {
|
|
goto cleanup;
|
|
}
|
|
|
|
/* Demo 2: Sign an event */
|
|
rc = demo_sign_event(signer, pubkey_hex);
|
|
if (rc != NOSTR_SUCCESS) {
|
|
goto cleanup;
|
|
}
|
|
|
|
/* Demo 3: NIP-44 encrypt/decrypt */
|
|
rc = demo_nip44(signer, pubkey_hex);
|
|
|
|
cleanup:
|
|
printf("\n=== Summary ===\n");
|
|
if (rc == NOSTR_SUCCESS) {
|
|
printf("All demos completed successfully.\n");
|
|
} else {
|
|
printf("Demo failed with error code %d.\n", rc);
|
|
}
|
|
|
|
nostr_signer_free(signer);
|
|
nostr_cleanup();
|
|
return (rc == NOSTR_SUCCESS) ? 0 : 1;
|
|
}
|