diff --git a/Makefile b/Makefile index 2aafed9..ee83814 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,7 @@ EXAMPLE_GET_PUBLIC_KEY_TARGET := $(BUILD_DIR)/example_get_public_key_client EXAMPLE_SIGN_EVENT_TARGET := $(BUILD_DIR)/example_sign_event_client EXAMPLE_GET_PUBKEY_TCP_TARGET := $(BUILD_DIR)/example_get_pubkey_tcp EXAMPLE_GET_PUBKEY_QREXEC_TARGET := $(BUILD_DIR)/example_get_pubkey_qrexec +DEMO_C99_TARGET := $(BUILD_DIR)/demo_c99 .PHONY: all lib dev static static-debug static-arm64 firmware-feather test test-integration test-mnemonic test-mnemonic-input test-role test-selector test-enforcement test-dispatcher test-policy test-socket-name test-auth-envelope test-qrexec-auth examples test-client clean @@ -110,7 +111,7 @@ test-qrexec-auth: $(TEST_QREXEC_AUTH_TARGET) $(TARGET_DEV) test-client: examples -examples: $(EXAMPLE_GET_PUBLIC_KEY_TARGET) $(EXAMPLE_SIGN_EVENT_TARGET) $(EXAMPLE_GET_PUBKEY_TCP_TARGET) $(EXAMPLE_GET_PUBKEY_QREXEC_TARGET) +examples: $(EXAMPLE_GET_PUBLIC_KEY_TARGET) $(EXAMPLE_SIGN_EVENT_TARGET) $(EXAMPLE_GET_PUBKEY_TCP_TARGET) $(EXAMPLE_GET_PUBKEY_QREXEC_TARGET) $(DEMO_C99_TARGET) $(TEST_MNEMONIC_TARGET): $(TEST_DIR)/test_mnemonic.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c @mkdir -p $(BUILD_DIR) @@ -172,5 +173,9 @@ $(EXAMPLE_GET_PUBKEY_QREXEC_TARGET): $(EXAMPLES_DIR)/get_pubkey_qrexec.c @mkdir -p $(BUILD_DIR) $(CC) $(CFLAGS) $(EXAMPLES_DIR)/get_pubkey_qrexec.c -o $(EXAMPLE_GET_PUBKEY_QREXEC_TARGET) $(LDFLAGS) +$(DEMO_C99_TARGET): $(CLIENT_DIR)/demo_c99.c + @mkdir -p $(BUILD_DIR) + $(CC) $(CFLAGS) $(CLIENT_DIR)/demo_c99.c -o $(DEMO_C99_TARGET) $(LDFLAGS) + clean: rm -rf $(BUILD_DIR) diff --git a/client/demo_c99.c b/client/demo_c99.c new file mode 100644 index 0000000..e490d63 --- /dev/null +++ b/client/demo_c99.c @@ -0,0 +1,318 @@ +/* + * 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) + * + * 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 [nostr_index] + * ./build/demo_c99 nostr_signer 1 + * + * If no nostr_index is given, defaults to 0. + */ + +#include +#include +#include +#include +#include + +#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 [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; +} diff --git a/src/main.c b/src/main.c index eb8781b..be8d7da 100644 --- a/src/main.c +++ b/src/main.c @@ -503,8 +503,8 @@ int socket_name_random(char *out, size_t out_len); /* Version information (auto-updated by build/version tooling) */ #define NSIGNER_VERSION_MAJOR 0 #define NSIGNER_VERSION_MINOR 0 -#define NSIGNER_VERSION_PATCH 42 -#define NSIGNER_VERSION "v0.0.42" +#define NSIGNER_VERSION_PATCH 43 +#define NSIGNER_VERSION "v0.0.43" /* NSIGNER_HEADERLESS_DECLS_END */